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 # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from pygal import ( from pygal import *
Line, Bar, XY, Pie, Radar, StackedBar, Config, from pygal.style import *
StackedLine, HorizontalBar, HorizontalStackedBar)
from pygal.style import NeonStyle
from math import cos, sin from math import cos, sin
lnk = lambda v: {'value': v, 'xlink': 'javascript:alert("Test %s")' % v}
bar = Bar() bar = Bar(style=styles['neon'])
rng = [-6, -19, 0, -1, 2] bar.add('1234', [
bar.add('test1', rng) {'value': 10, 'label': 'Ten', 'xlink': 'http://google.com?q=10'},
bar.add('test2', map(abs, rng)) {'value': 20, 'label': 'Twenty', 'xlink': 'http://google.com?q=20'},
bar.add('inc', [None, 1, None, 2]) {'value': 30, 'label': 'Thirty', 'xlink': 'http://google.com?q=30'},
bar.x_labels = map(str, rng) {'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.fill = True
# bar.render_to_file('out-bar.svg') bar.render_to_file('out-bar.svg')
hbar = HorizontalBar() hbar = HorizontalBar()
rng = [18, 9, 7, 3, 1, None, -5] rng = [18, 9, 7, 3, 1, None, -5]
@ -71,8 +75,8 @@ hstackedbar.add('@@@@@@@', rng)
hstackedbar.add('++++++', rng2) hstackedbar.add('++++++', rng2)
hstackedbar.add('--->', rng3) hstackedbar.add('--->', rng3)
hstackedbar.render_to_file('out-horizontalstackedbar1.svg') # hstackedbar.render_to_file('out-horizontalstackedbar1.svg')
hstackedbar.render_to_file('out-horizontalstackedbar2.svg') # hstackedbar.render_to_file('out-horizontalstackedbar2.svg')
line = Line(Config(style=NeonStyle, line = Line(Config(style=NeonStyle,
zero=.0001, fill=True, zero=.0001, fill=True,
@ -93,7 +97,7 @@ rng = range(-30, 31, 1)
# line.add('_', [2 ** -3, 2.9 ** -8, 2]) # line.add('_', [2 ** -3, 2.9 ** -8, 2])
# line.add('_', [.001, .0001, .00001]) # line.add('_', [.001, .0001, .00001])
# line.add('_', [1 + 10 ** 10, 3 + 10 ** 10, 2 + 10 ** 10]) # 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.x_labels = map(str, rng)
line.title = "Line test" line.title = "Line test"
# line.interpolate = "cubic" # line.interpolate = "cubic"
@ -119,14 +123,17 @@ xy.title = "XY test"
# xy.render_to_file('out-xy.svg') # xy.render_to_file('out-xy.svg')
pie = Pie(Config(style=NeonStyle)) pie = Pie(Config(style=NeonStyle))
pie.add('test', [11, 8, 21]) pie.add('test', [lnk(11), 8, 21])
pie.add('test2', [29, None, 9]) pie.add('test2', [lnk(29), None, 9])
pie.add('test3', [24, 10, 32]) 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('test5', [17, 5, 10])
pie.add('test6', [None, None, 10]) pie.add('test6', [None, None, 10])
pie.title = "Pie test" # pie.add('test', {'value': 11, 'xlink': 'javascript:alert("lol 11")'})
# pie.render_to_file('out-pie.svg') # pie.add('test2', 1)
# pie.add('test3', 5)
# pie.title = "Pie test"
pie.render_to_file('out-pie.svg')
config = Config() config = Config()
config.fill = True config.fill = True
@ -135,9 +142,9 @@ config.x_labels = (
'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white') 'black', 'red', 'blue', 'yellow', 'orange', 'green', 'white')
config.interpolate = 'nearest' config.interpolate = 'nearest'
radar = Radar(config) 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.add('test2', [10, 2, 0, 5, 1, 9, 4])
radar.title = "Radar test" 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 __future__ import division
from pygal.graph.graph import Graph 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): class Bar(Graph):
@ -60,6 +60,7 @@ class Bar(Graph):
# (x,y) (X,Y) # (x,y) (X,Y)
# #
# x and y are left range coords and X, Y right ones # x and y are left range coords and X, Y right ones
metadata = serie.metadata[i]
val = self._format(values[i][1][1]) val = self._format(values[i][1][1])
if self.horizontal: if self.horizontal:
x, y, X, Y = Y, X, y, x x, y, X, Y = Y, X, y, x
@ -87,7 +88,11 @@ class Bar(Graph):
y = y + height y = y + height
height = -height height = -height
y -= shift y -= shift
bar = self.svg.node(bars, class_='bar')
bar = decorate(
self.svg,
self.svg.node(bars, class_='bar'),
metadata)
self.svg.transposable_node( self.svg.transposable_node(
bar, 'rect', bar, 'rect',
x=x, x=x,

3
pygal/graph/base.py

@ -130,9 +130,6 @@ class BaseGraph(object):
"""Check if there is any data""" """Check if there is any data"""
if len(self.series) == 0: if len(self.series) == 0:
return False 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: if sum(map(len, map(lambda s: s.values, self.series))) == 0:
return False return False
return True return True

8
pygal/graph/line.py

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

102
pygal/graph/pie.py

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

1
pygal/graph/radar.py

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

12
pygal/serie.py

@ -25,10 +25,20 @@ class Serie(object):
"""Serie containing title, values and the graph serie index""" """Serie containing title, values and the graph serie index"""
def __init__(self, title, values, index): def __init__(self, title, values, index):
self.title = title 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 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): class Label(object):
"""A label with his position""" """A label with his position"""
def __init__(self, label, pos): def __init__(self, label, pos):

49
pygal/svg.py

@ -25,6 +25,7 @@ from __future__ import division
import io import io
import os import os
from lxml import etree from lxml import etree
from math import cos, sin, pi
from pygal.util import template, coord_format from pygal.util import template, coord_format
from pygal import __version__ from pygal import __version__
@ -88,6 +89,9 @@ class Svg(object):
elif key.endswith('_'): elif key.endswith('_'):
attrib[key.rstrip('_')] = attrib[key] attrib[key.rstrip('_')] = attrib[key]
del 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) return etree.SubElement(parent, tag, attrib)
def transposable_node(self, parent=None, tag='g', attrib=None, **extras): def transposable_node(self, parent=None, tag='g', attrib=None, **extras):
@ -114,6 +118,51 @@ class Svg(object):
self.node(node, 'path', self.node(node, 'path',
d=root % (origin, line), **kwargs) 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): def pre_render(self, no_data=False):
"""Last things to do before rendering""" """Last things to do before rendering"""
self.add_style(self.graph.base_css or os.path.join( 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" ORDERS = u"yzafpnµm kMGTPEZY"
def get_value(val):
if isinstance(val, dict):
return val['value']
return val
def float_format(number): def float_format(number):
"""Format a float to a precision of 3, without zeroes or dots""" """Format a float to a precision of 3, without zeroes or dots"""
return ("%.3f" % number).rstrip('0').rstrip('.') return ("%.3f" % number).rstrip('0').rstrip('.')
@ -182,6 +188,16 @@ def get_texts_box(texts, fs):
return (fs, text_len(max_len, 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/ # Stolen from brownie http://packages.python.org/Brownie/
class cached_property(object): class cached_property(object):
"""Optimize a static property""" """Optimize a static property"""

Loading…
Cancel
Save