Browse Source

Start supporting multivalue in gauge

pull/307/head
Florian Mounier 9 years ago
parent
commit
0d60a816f5
  1. 13
      demo/moulinrouge/tests.py
  2. 8
      pygal/css/graph.css
  3. 80
      pygal/graph/solidgauge.py
  4. 7
      pygal/style.py
  5. 160
      pygal/svg.py
  6. 22
      pygal/util.py

13
demo/moulinrouge/tests.py

@ -209,17 +209,20 @@ def get_test_routes(app):
print_values=True, print_values=True,
human_readable=True) human_readable=True)
gauge.title = 'Hello World!' gauge.title = 'Hello World!'
percent_formatter = lambda x: '{}%'.format(x) percent_formatter = lambda x: '{:.10g}%'.format(x)
dollar_formatter = lambda x: '{}$'.format(x) dollar_formatter = lambda x: '{:.10g}$'.format(x)
gauge.value_formatter = percent_formatter gauge.value_formatter = percent_formatter
gauge.add('Series 1', [{'value': 225000, 'maxvalue': 1275000}], gauge.add('Series 1', [{'value': 225000, 'maxvalue': 1275000}],
value_formatter=dollar_formatter) formatter=dollar_formatter)
gauge.add('Series 2', [{'value': 110, 'maxvalue': 100}]) gauge.add('Series 2', [{'value': 110, 'maxvalue': 100}])
gauge.add('Series 3', [{'value': 3}]) gauge.add('Series 3', [{'value': 3}])
gauge.add('Series 4', [{'value': 51, 'maxvalue': 100}]) gauge.add(
'Series 4', [
{'value': 51, 'maxvalue': 100},
{'value': 12, 'maxvalue': 100}])
gauge.add('Series 5', [{'value': 79, 'maxvalue': 100}]) gauge.add('Series 5', [{'value': 79, 'maxvalue': 100}])
gauge.add('Series 6', [{'value': 99, 'maxvalue': 100}]) gauge.add('Series 6', 99)
gauge.add('Series 7', [{'value': 100, 'maxvalue': 100}]) gauge.add('Series 7', [{'value': 100, 'maxvalue': 100}])
return gauge.render_response() return gauge.render_response()

8
pygal/css/graph.css

@ -141,12 +141,12 @@
visibility: visible; visibility: visible;
} }
{{ id }}.background-shade { {{ id }}.gauge-background {
fill: {{ style.gauge_background_color }}; fill: {{ style.value_background }};
stroke: none; stroke: none;
} }
{{ id }}.bg-lines { {{ id }}.bg-lines {
stroke: {{ style.square_border_color }}; stroke: {{ style.background }};
stroke-width: {{ style.square_border_width }}; stroke-width: 2px;
} }

80
pygal/graph/solidgauge.py

@ -33,43 +33,65 @@ from pygal.util import alter, decorate
class SolidGauge(Graph): class SolidGauge(Graph):
def gaugify( def gaugify(self, serie, squares, sq_dimensions, current_square):
self, serie, startangle, squares, sq_dimensions, current_square):
serie_node = self.svg.serie(serie) serie_node = self.svg.serie(serie)
metadata = serie.metadata.get(0) or {}
maxvalue = metadata.get('maxvalue', 100)
if self.half_pie: if self.half_pie:
start_angle = 3*pi/2
center = ( center = (
(current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.), (current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.),
(current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 4)) (current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 4))
end_angle = pi / 2
else: else:
start_angle = 0
center = ( center = (
(current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.), (current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.),
(current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 2.)) (current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 2.))
end_angle = 2 * pi
radius = min([sq_dimensions[0]/2, sq_dimensions[1]/2]) maxvalue = serie.metadata.get(0, {}).get('maxvalue', 100)
value = serie.values[0] radius = min([sq_dimensions[0]/2, sq_dimensions[1]/2]) * .9
ratio = min(value, maxvalue) / maxvalue
if self.half_pie:
angle = 2 * pi * ratio / 2
endangle = pi / 2
else:
angle = 2 * pi * ratio
endangle = 2 * pi
value = self._format(value)
gauge_ = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="gauge"),
metadata)
big_radius = radius * .9
small_radius = radius * serie.inner_radius small_radius = radius * serie.inner_radius
alter(self.svg.solidgauge(
serie_node, gauge_, big_radius, small_radius, self.svg.gauge_background(
angle, startangle, center, value, 0, metadata, serie_node, start_angle, center, radius, small_radius, end_angle,
self.half_pie, endangle, self._format(maxvalue)), metadata) self.half_pie)
sum_ = 0
for i, value in enumerate(serie.values):
if value is None:
continue
ratio = min(value, maxvalue) / maxvalue
if self.half_pie:
angle = 2 * pi * ratio / 2
else:
angle = 2 * pi * ratio
val = self._format(serie, i)
metadata = serie.metadata.get(i)
gauge_ = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="gauge"),
metadata)
alter(
self.svg.solid_gauge(
serie_node, gauge_, radius, small_radius,
angle, start_angle, center, val, i, metadata,
self.half_pie, end_angle,
self._serie_format(serie, maxvalue)),
metadata)
start_angle += angle
sum_ += value
x, y = center
self.svg.node(
serie_node['text_overlay'], 'text',
class_='value solidgauge-sum',
x=x,
y=y + self.style.value_font_size / 3,
attrib={'text-anchor': 'middle'}
).text = self._serie_format(serie, sum_)
def _compute_x_labels(self): def _compute_x_labels(self):
pass pass
@ -81,15 +103,11 @@ class SolidGauge(Graph):
"""Draw all the serie slices""" """Draw all the serie slices"""
squares = self._squares() squares = self._squares()
sq_dimensions = self.add_squares(squares) sq_dimensions = self.add_squares(squares)
if self.half_pie:
startangle = 3*pi/2
else:
startangle = 0
for index, serie in enumerate(self.series): for index, serie in enumerate(self.series):
current_square = self._current_square(squares, index) current_square = self._current_square(squares, index)
self.gaugify( self.gaugify(
serie, startangle, squares, sq_dimensions, current_square) serie, squares, sq_dimensions, current_square)
def _squares(self): def _squares(self):

7
pygal/style.py

@ -32,6 +32,7 @@ class Style(object):
plot_background = 'rgba(255, 255, 255, 1)' plot_background = 'rgba(255, 255, 255, 1)'
background = 'rgba(249, 249, 249, 1)' background = 'rgba(249, 249, 249, 1)'
value_background = 'rgba(229, 229, 229, 1)'
foreground = 'rgba(0, 0, 0, .87)' foreground = 'rgba(0, 0, 0, .87)'
foreground_strong = 'rgba(0, 0, 0, 1)' foreground_strong = 'rgba(0, 0, 0, 1)'
foreground_subtle = 'rgba(0, 0, 0, .54)' foreground_subtle = 'rgba(0, 0, 0, .54)'
@ -81,17 +82,13 @@ class Style(object):
'#FFEB3B', # 12 '#FFEB3B', # 12
'#673AB7', # 3 '#673AB7', # 3
'#00BCD4', # 7 '#00BCD4', # 7
'#CDDC39', # 11 '#CDDC39', # 11b
'#795548', # 16
'#9E9E9E', # 17 '#9E9E9E', # 17
'#607D8B', # 18 '#607D8B', # 18
) )
value_colors = () value_colors = ()
ci_colors = () ci_colors = ()
gauge_background_color = "#e5e5e5"
square_border_color = 'rgba(249, 249, 249, 1)'
square_border_width = '2px'
def __init__(self, **kwargs): def __init__(self, **kwargs):
"""Create the style""" """Create the style"""

160
pygal/svg.py

@ -26,8 +26,10 @@ import os
import json import json
from datetime import date, datetime from datetime import date, datetime
from numbers import Number from numbers import Number
from math import cos, sin, pi from math import pi
from pygal.util import template, minify_css from pygal.util import (
template, minify_css,
coord_project, coord_diff, coord_format, coord_dual, coord_abs_project)
from pygal import __version__ from pygal import __version__
@ -255,14 +257,6 @@ class Svg(object):
self, serie_node, node, radius, small_radius, self, serie_node, node, radius, small_radius,
angle, start_angle, center, val, i, metadata): angle, start_angle, center, val, i, metadata):
"""Draw a pie slice""" """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: if angle == 2 * pi:
rv = self.node( rv = self.node(
node, 'circle', node, 'circle',
@ -271,21 +265,21 @@ class Svg(object):
r=radius, r=radius,
class_='slice reactive tooltip-trigger') class_='slice reactive tooltip-trigger')
elif angle > 0: elif angle > 0:
to = [absolute_project(radius, start_angle), to = [coord_abs_project(center,radius, start_angle),
absolute_project(radius, start_angle + angle), coord_abs_project(center,radius, start_angle + angle),
absolute_project(small_radius, start_angle + angle), coord_abs_project(center,small_radius, start_angle + angle),
absolute_project(small_radius, start_angle)] coord_abs_project(center,small_radius, start_angle)]
rv = self.node( rv = self.node(
node, 'path', node, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0], to[0],
get_radius(radius), int(angle > pi), to[1], coord_dual(radius), int(angle > pi), to[1],
to[2], to[2],
get_radius(small_radius), int(angle > pi), to[3]), coord_dual(small_radius), int(angle > pi), to[3]),
class_='slice reactive tooltip-trigger') class_='slice reactive tooltip-trigger')
else: else:
rv = None rv = None
x, y = diff(center, project( x, y = coord_diff(center, coord_project(
(radius + small_radius) / 2, start_angle + angle / 2)) (radius + small_radius) / 2, start_angle + angle / 2))
self.graph._tooltip_data( self.graph._tooltip_data(
@ -295,66 +289,66 @@ class Svg(object):
self.graph._static_value(serie_node, val, x, y, metadata) self.graph._static_value(serie_node, val, x, y, metadata)
return rv return rv
def solidgauge( def gauge_background(
self, serie_node, start_angle, center, radius, small_radius,
end_angle, half_pie):
to_shade = [
coord_abs_project(center, radius, start_angle),
coord_abs_project(center, radius, end_angle),
coord_abs_project(center, small_radius, end_angle),
coord_abs_project(center, small_radius, start_angle)]
self.node(
serie_node['plot'], 'path',
d='M%s A%s 0 1 1 %s L%s A%s 0 1 0 %s z' % (
to_shade[0],
coord_dual(radius),
to_shade[1],
to_shade[2],
coord_dual(small_radius),
to_shade[3]),
class_='gauge-background reactive')
def solid_gauge(
self, serie_node, node, radius, small_radius, self, serie_node, node, radius, small_radius,
angle, start_angle, center, val, i, metadata, half_pie, endangle, angle, start_angle, center, val, i, metadata, half_pie, end_angle,
maxvalue): maxvalue):
"""Draw a solid gauge slice and background slice""" """Draw a solid gauge slice and background 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: if angle == 2 * pi:
to = [absolute_project(radius, start_angle), to = [coord_abs_project(center, radius, start_angle),
absolute_project(radius, start_angle + angle - 0.0001), coord_abs_project(
absolute_project(small_radius, start_angle + angle - 0.0001), center, radius, start_angle + angle - 0.0001),
absolute_project(small_radius, start_angle)] coord_abs_project(
center, small_radius, start_angle + angle - 0.0001),
coord_abs_project(center, small_radius, start_angle)]
self.node( self.node(
node, 'path', node, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0], to[0],
get_radius(radius), coord_dual(radius),
int(angle > pi), int(angle > pi),
to[1], to[1],
to[2], to[2],
get_radius(small_radius), coord_dual(small_radius),
int(angle > pi), int(angle > pi),
to[3]), to[3]),
class_='slice reactive tooltip-trigger') class_='slice reactive tooltip-trigger')
elif angle > 0: elif angle > 0:
to_shade = [absolute_project(radius, start_angle+angle), to = [coord_abs_project(center, radius, start_angle),
absolute_project(radius, endangle), coord_abs_project(center, radius, start_angle + angle),
absolute_project(small_radius, endangle), coord_abs_project(center, small_radius, start_angle + angle),
absolute_project(small_radius, start_angle+angle)] coord_abs_project(center, 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_shade[0],
get_radius(radius),
int(angle > pi) if half_pie else int(angle < pi),
to_shade[1],
to_shade[2],
get_radius(small_radius),
int(angle > pi) if half_pie else int(angle < pi),
to_shade[3]),
class_='background-shade reactive')
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)]
if half_pie: if half_pie:
begin_end = [ begin_end = [
diff(center, coord_diff(
project(radius-(radius-small_radius)/2, start_angle)), center,
diff(center, coord_project(
project(radius-(radius-small_radius)/2, endangle))] radius-(radius-small_radius)/2, start_angle)),
coord_diff(
center,
coord_project(
radius-(radius-small_radius)/2, end_angle))]
pos = 0 pos = 0
for i in begin_end: for i in begin_end:
self.node( self.node(
@ -367,43 +361,49 @@ class Svg(object):
).text = '{}'.format(0 if pos == 0 else maxvalue) ).text = '{}'.format(0 if pos == 0 else maxvalue)
pos += 1 pos += 1
else: else:
to_labels = [absolute_project(radius-(radius-small_radius)/2, 0), to_labels = [
absolute_project(radius-(radius-small_radius)/2, 2*359.5/360*pi)] coord_abs_project(
self.node(self.defs, 'path', id='valuePath-%s%s' % center, center, radius-(radius-small_radius)/2, 0),
d='M%s A%s 0 1 1 %s' % ( coord_abs_project(
to_labels[0], get_radius(radius-(radius-small_radius)/2), center, radius-(radius-small_radius)/2, 2*359.5/360*pi)
to_labels[1] ]
), stroke='#000000', width='3px') self.node(
self.defs, 'path', id='valuePath-%s%s' % center,
d='M%s A%s 0 1 1 %s' % (
to_labels[0],
coord_dual(radius-(radius-small_radius)/2),
to_labels[1]
), stroke='#000000', width='3px')
text_ = self.node(node, 'text', x=10, y=100, stroke='black') text_ = self.node(node, 'text', x=10, y=100, stroke='black')
self.node(text_, 'textPath', class_='maxvalue reactive', self.node(
attrib={'href': '#valuePath-%s%s' % center, text_, 'textPath', class_='maxvalue reactive',
'startOffset': '%s' % (97-1.2*len(str(maxvalue))) + '%', attrib={
'text-anchor': 'start', 'href': '#valuePath-%s%s' % center,
'font-size': (radius-small_radius)/2, 'startOffset': '%s' % (
'fill': '#999999'} 97-1.2*len(str(maxvalue))) + '%',
).text = maxvalue 'text-anchor': 'start',
'font-size': (radius-small_radius)/2,
'fill': '#999999'}
).text = maxvalue
self.node( self.node(
node, 'path', node, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to[0], to[0],
get_radius(radius), coord_dual(radius),
int(angle > pi), int(angle > pi),
to[1], to[1],
to[2], to[2],
get_radius(small_radius), coord_dual(small_radius),
int(angle > pi), int(angle > pi),
to[3]), to[3]),
class_='slice reactive tooltip-trigger') class_='slice reactive tooltip-trigger')
else: else:
return return
x, y = coord_diff(center, coord_project(
x, y = center
self.graph._static_value(serie_node, val, x, y, metadata, 'middle')
x, y = diff(center, project(
(radius + small_radius) / 2, start_angle + angle / 2)) (radius + small_radius) / 2, start_angle + angle / 2))
self.graph._static_value(serie_node, val, x, y, metadata, 'middle')
self.graph._tooltip_data( self.graph._tooltip_data(
node, val, x, y, "centered", node, val, x, y, "centered",
self.graph._x_labels and self.graph._x_labels[i][0]) self.graph._x_labels and self.graph._x_labels[i][0])

22
pygal/util.py

@ -24,7 +24,7 @@ from __future__ import division
import re import re
from decimal import Decimal from decimal import Decimal
from math import ceil, floor, log10, pi from math import ceil, floor, log10, pi, cos, sin
from pygal._compat import to_unicode, u from pygal._compat import to_unicode, u
@ -344,3 +344,23 @@ def filter_kwargs(fun, kwargs):
return {} return {}
args = fun.__code__.co_varnames[1:] args = fun.__code__.co_varnames[1:]
return dict((k, v) for k, v in kwargs.items() if k in args) return dict((k, v) for k, v in kwargs.items() if k in args)
def coord_project(rho, alpha):
return rho * sin(-alpha), rho * cos(-alpha)
def coord_diff(x, y):
return (x[0] - y[0], x[1] - y[1])
def coord_format(x):
return '%f %f' % x
def coord_dual(r):
return coord_format((r, r))
def coord_abs_project(center, rho, theta):
return coord_format(coord_diff(center, coord_project(rho, theta)))

Loading…
Cancel
Save