Browse Source

Support links and various metadata on values

pull/8/head
Florian Mounier 13 years ago
parent
commit
77c4197061
  1. 49
      demo/simple_test.py
  2. 9
      pygal/graph/bar.py
  3. 3
      pygal/graph/base.py
  4. 8
      pygal/graph/line.py
  5. 102
      pygal/graph/pie.py
  6. 1
      pygal/graph/radar.py
  7. 12
      pygal/serie.py
  8. 49
      pygal/svg.py
  9. 16
      pygal/util.py

49
demo/simple_test.py

@ -17,22 +17,26 @@
#
# You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import (
Line, Bar, XY, Pie, Radar, StackedBar, Config,
StackedLine, HorizontalBar, HorizontalStackedBar)
from pygal.style import NeonStyle
from pygal import *
from pygal.style import *
from math import cos, sin
lnk = lambda v: {'value': v, 'xlink': 'javascript:alert("Test %s")' % v}
bar = Bar()
rng = [-6, -19, 0, -1, 2]
bar.add('test1', rng)
bar.add('test2', map(abs, rng))
bar.add('inc', [None, 1, None, 2])
bar.x_labels = map(str, rng)
bar = Bar(style=styles['neon'])
bar.add('1234', [
{'value': 10, 'label': 'Ten', 'xlink': 'http://google.com?q=10'},
{'value': 20, 'label': 'Twenty', 'xlink': 'http://google.com?q=20'},
{'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30'},
{'value': 40, 'label': 'Forty', 'xlink': 'http://google.com?q=40'}
])
bar.add('4321', [40, 30, 20, 10])
bar.x_labels = map(str, range(1, 5))
bar.fill = True
# bar.render_to_file('out-bar.svg')
bar.render_to_file('out-bar.svg')
hbar = HorizontalBar()
rng = [18, 9, 7, 3, 1, None, -5]
@ -71,8 +75,8 @@ hstackedbar.add('@@@@@@@', rng)
hstackedbar.add('++++++', rng2)
hstackedbar.add('--->', rng3)
hstackedbar.render_to_file('out-horizontalstackedbar1.svg')
hstackedbar.render_to_file('out-horizontalstackedbar2.svg')
# hstackedbar.render_to_file('out-horizontalstackedbar1.svg')
# hstackedbar.render_to_file('out-horizontalstackedbar2.svg')
line = Line(Config(style=NeonStyle,
zero=.0001, fill=True,
@ -93,7 +97,7 @@ rng = range(-30, 31, 1)
# line.add('_', [2 ** -3, 2.9 ** -8, 2])
# line.add('_', [.001, .0001, .00001])
# line.add('_', [1 + 10 ** 10, 3 + 10 ** 10, 2 + 10 ** 10])
line.add('_', [1, -4, 2, 8, -2])
line.add('_', [1, lnk(4), None, 2, 8, lnk(-2), None, lnk(2)])
line.x_labels = map(str, rng)
line.title = "Line test"
# line.interpolate = "cubic"
@ -119,14 +123,17 @@ xy.title = "XY test"
# xy.render_to_file('out-xy.svg')
pie = Pie(Config(style=NeonStyle))
pie.add('test', [11, 8, 21])
pie.add('test2', [29, None, 9])
pie.add('test', [lnk(11), 8, 21])
pie.add('test2', [lnk(29), None, 9])
pie.add('test3', [24, 10, 32])
pie.add('test4', [20, 18, 9])
pie.add('test4', [20, lnk(18), 9])
pie.add('test5', [17, 5, 10])
pie.add('test6', [None, None, 10])
pie.title = "Pie test"
# pie.render_to_file('out-pie.svg')
# pie.add('test', {'value': 11, 'xlink': 'javascript:alert("lol 11")'})
# pie.add('test2', 1)
# pie.add('test3', 5)
# pie.title = "Pie test"
pie.render_to_file('out-pie.svg')
config = Config()
config.fill = True
@ -135,9 +142,9 @@ config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
config.interpolate = 'nearest'
radar = Radar(config)
radar.add('test', [1, 4, 1, 5, None, 2, 5])
radar.add('test', [1, 4, lnk(1), 5, None, 2, 5])
radar.add('test2', [10, 2, 0, 5, 1, 9, 4])
radar.title = "Radar test"
# radar.render_to_file('out-radar.svg')
radar.render_to_file('out-radar.svg')

9
pygal/graph/bar.py

@ -23,7 +23,7 @@ Bar chart
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import swap, ident, compute_scale
from pygal.util import swap, ident, compute_scale, decorate
class Bar(Graph):
@ -60,6 +60,7 @@ class Bar(Graph):
# (x,y) (X,Y)
#
# x and y are left range coords and X, Y right ones
metadata = serie.metadata[i]
val = self._format(values[i][1][1])
if self.horizontal:
x, y, X, Y = Y, X, y, x
@ -87,7 +88,11 @@ class Bar(Graph):
y = y + height
height = -height
y -= shift
bar = self.svg.node(bars, class_='bar')
bar = decorate(
self.svg,
self.svg.node(bars, class_='bar'),
metadata)
self.svg.transposable_node(
bar, 'rect',
x=x,

3
pygal/graph/base.py

@ -130,9 +130,6 @@ class BaseGraph(object):
"""Check if there is any data"""
if len(self.series) == 0:
return False
for serie in self.series:
if not hasattr(serie.values, '__iter__'):
serie.values = [serie.values]
if sum(map(len, map(lambda s: s.values, self.series))) == 0:
return False
return True

8
pygal/graph/line.py

@ -22,7 +22,7 @@ Line chart
"""
from __future__ import division
from pygal.graph.graph import Graph
from pygal.util import cached_property, compute_scale
from pygal.util import cached_property, compute_scale, decorate
class Line(Graph):
@ -60,6 +60,7 @@ class Line(Graph):
if None in (x, y):
continue
metadata = serie.metadata[i]
classes = []
if x > self.view.width / 2:
classes.append('left')
@ -67,7 +68,10 @@ class Line(Graph):
classes.append('top')
classes = ' '.join(classes)
dots = self.svg.node(serie_node['overlay'], class_="dots")
dots = decorate(
self.svg,
self.svg.node(serie_node['overlay'], class_="dots"),
metadata)
val = self._get_value(serie.points, i)
self.svg.node(dots, 'circle', cx=x, cy=y, r=2.5,
class_='dot reactive tooltip-trigger')

102
pygal/graph/pie.py

@ -22,67 +22,48 @@ Pie chart
"""
from __future__ import division
from pygal.util import decorate
from pygal.graph.graph import Graph
from math import cos, sin, pi
project = lambda rho, alpha: (
rho * sin(-alpha), rho * cos(-alpha))
diff = lambda x, y: (x[0] - y[0], x[1] - y[1])
fmt = lambda x: '%f %f' % x
get_radius = lambda r: fmt(tuple([r] * 2))
from math import pi
class Pie(Graph):
"""Pie graph"""
def slice(self, serie_node, start_angle, angle, perc,
small=False):
def slice(self, serie_node, start_angle, serie, total):
"""Make a serie slice"""
val = '{0:.2%}'.format(perc)
slices = self.svg.node(serie_node['plot'], class_="slices")
slice_ = self.svg.node(slices, class_="slice")
serie_angle = 0
original_start_angle = start_angle
center = ((self.width - self.margin.x) / 2.,
(self.height - self.margin.y) / 2.)
r = min(center)
if small:
small_r = r * .9
else:
r = r * .9
small_r = 0
if perc == 1:
self.svg.node(slice_, 'circle',
cx=center[0],
cy=center[1],
r=r,
class_='slice reactive tooltip-trigger')
else:
absolute_project = lambda rho, theta: fmt(
diff(center, project(rho, theta)))
to = [absolute_project(r, start_angle),
absolute_project(r, start_angle + angle),
absolute_project(small_r, start_angle + angle),
absolute_project(small_r, start_angle)]
self.svg.node(slice_, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0],
get_radius(r), int(angle > pi), to[1],
to[2],
get_radius(small_r), int(angle > pi), to[3]),
class_='slice reactive tooltip-trigger')
self.svg.node(slice_, 'desc', class_="value").text = val
tooltip_position = map(
str, diff(center, project(
(r + small_r) / 2, start_angle + angle / 2)))
self.svg.node(slice_, 'desc',
class_="x centered").text = tooltip_position[0]
self.svg.node(slice_, 'desc',
class_="y centered").text = tooltip_position[1]
if self.print_values:
self.svg.node(
serie_node['text_overlay'], 'text',
class_='centered',
x=tooltip_position[0],
y=tooltip_position[1]
).text = val if self.print_zeroes or val != '0%' else ''
radius = min(center)
for i, val in enumerate(serie.values):
perc = val / total
angle = 2 * pi * perc
serie_angle += angle
val = '{0:.2%}'.format(perc)
metadata = serie.metadata[i]
slice_ = decorate(
self.svg,
self.svg.node(slices, class_="slice"),
metadata)
if len(serie.values) > 1:
small_radius = radius * .9
else:
radius = radius * .9
small_radius = 0
self.svg.slice(serie_node,
slice_, radius, small_radius, angle, start_angle, center, val)
start_angle += angle
if len(serie.values) > 1:
self.svg.slice(serie_node,
slice_, radius * .9, 0, serie_angle,
original_start_angle, center, val)
return serie_angle
def _compute(self):
for serie in self.series:
@ -90,25 +71,12 @@ class Pie(Graph):
return super(Pie, self)._compute()
def _plot(self):
total = float(sum(map(sum, map(lambda x: x.values, self.series))))
total = sum(map(sum, map(lambda x: x.values, self.series)))
if total == 0:
return
current_angle = 0
for serie in self.series:
angle = 2 * pi * sum(serie.values) / total
self.slice(
self._serie(serie.index),
current_angle,
angle, sum(serie.values) / total)
if len(serie.values) > 1:
small_current_angle = current_angle
for val in serie.values:
small_angle = 2 * pi * val / total
self.slice(
self._serie(serie.index),
small_current_angle,
small_angle, val / total,
True)
small_current_angle += small_angle
angle = self.slice(
self._serie(serie.index), current_angle, serie, total)
current_angle += angle

1
pygal/graph/radar.py

@ -107,6 +107,7 @@ class Radar(Line):
for serie in self.series:
vals = list(serie.values)
vals.append(vals[0])
serie.metadata.append(serie.metadata[0])
vals = [val if val != None else 0 for val in vals]
serie.points = [
(v, x_pos[i])

12
pygal/serie.py

@ -25,10 +25,20 @@ class Serie(object):
"""Serie containing title, values and the graph serie index"""
def __init__(self, title, values, index):
self.title = title
self.values = values
if isinstance(values, dict) or not hasattr(values, '__iter__'):
values = [values]
self.metadata = map(Value, values)
self.values = [value.value for value in self.metadata]
self.index = index
class Value(object):
def __init__(self, value):
if not isinstance(value, dict):
value = {'value': value}
self.__dict__.update(value)
class Label(object):
"""A label with his position"""
def __init__(self, label, pos):

49
pygal/svg.py

@ -25,6 +25,7 @@ from __future__ import division
import io
import os
from lxml import etree
from math import cos, sin, pi
from pygal.util import template, coord_format
from pygal import __version__
@ -88,6 +89,9 @@ class Svg(object):
elif key.endswith('_'):
attrib[key.rstrip('_')] = attrib[key]
del attrib[key]
elif key == 'href':
attrib['{http://www.w3.org/1999/xlink}' + key] = attrib[key]
del attrib[key]
return etree.SubElement(parent, tag, attrib)
def transposable_node(self, parent=None, tag='g', attrib=None, **extras):
@ -114,6 +118,51 @@ class Svg(object):
self.node(node, 'path',
d=root % (origin, line), **kwargs)
def slice(self, serie_node, node, radius, small_radius,
angle, start_angle, center, val):
"""Draw a pie slice"""
project = lambda rho, alpha: (
rho * sin(-alpha), rho * cos(-alpha))
diff = lambda x, y: (x[0] - y[0], x[1] - y[1])
fmt = lambda x: '%f %f' % x
get_radius = lambda r: fmt(tuple([r] * 2))
absolute_project = lambda rho, theta: fmt(
diff(center, project(rho, theta)))
if angle == 2 * pi:
self.node(node, 'circle',
cx=center[0],
cy=center[1],
r=radius,
class_='slice reactive tooltip-trigger')
else:
to = [absolute_project(radius, start_angle),
absolute_project(radius, start_angle + angle),
absolute_project(small_radius, start_angle + angle),
absolute_project(small_radius, start_angle)]
self.node(node, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0],
get_radius(radius), int(angle > pi), to[1],
to[2],
get_radius(small_radius), int(angle > pi), to[3]),
class_='slice reactive tooltip-trigger')
self.node(node, 'desc', class_="value").text = val
tooltip_position = map(
str, diff(center, project(
(radius + small_radius) / 2, start_angle + angle / 2)))
self.node(node, 'desc',
class_="x centered").text = tooltip_position[0]
self.node(node, 'desc',
class_="y centered").text = tooltip_position[1]
if self.graph.print_values:
self.node(
serie_node['text_overlay'], 'text',
class_='centered',
x=tooltip_position[0],
y=tooltip_position[1]
).text = val if self.graph.print_zeroes or val != '0%' else ''
def pre_render(self, no_data=False):
"""Last things to do before rendering"""
self.add_style(self.graph.base_css or os.path.join(

16
pygal/util.py

@ -27,6 +27,12 @@ from math import floor, pi, log, log10, ceil
ORDERS = u"yzafpnµm kMGTPEZY"
def get_value(val):
if isinstance(val, dict):
return val['value']
return val
def float_format(number):
"""Format a float to a precision of 3, without zeroes or dots"""
return ("%.3f" % number).rstrip('0').rstrip('.')
@ -182,6 +188,16 @@ def get_texts_box(texts, fs):
return (fs, text_len(max_len, fs))
def decorate(svg, node, metadata):
if hasattr(metadata, 'xlink'):
return svg.node(node, 'a', href=metadata.xlink)
for key in dir(metadata):
if key not in ('xlink', 'value') and not key.startswith('_'):
svg.node(node, 'desc', class_=key).text = str(
getattr(metadata, key))
return node
# Stolen from brownie http://packages.python.org/Brownie/
class cached_property(object):
"""Optimize a static property"""

Loading…
Cancel
Save