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,
human_readable=True)
gauge.title = 'Hello World!'
percent_formatter = lambda x: '{}%'.format(x)
dollar_formatter = lambda x: '{}$'.format(x)
percent_formatter = lambda x: '{:.10g}%'.format(x)
dollar_formatter = lambda x: '{:.10g}$'.format(x)
gauge.value_formatter = percent_formatter
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 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 6', [{'value': 99, 'maxvalue': 100}])
gauge.add('Series 6', 99)
gauge.add('Series 7', [{'value': 100, 'maxvalue': 100}])
return gauge.render_response()

8
pygal/css/graph.css

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

80
pygal/graph/solidgauge.py

@ -33,43 +33,65 @@ from pygal.util import alter, decorate
class SolidGauge(Graph):
def gaugify(
self, serie, startangle, squares, sq_dimensions, current_square):
def gaugify(self, serie, squares, sq_dimensions, current_square):
serie_node = self.svg.serie(serie)
metadata = serie.metadata.get(0) or {}
maxvalue = metadata.get('maxvalue', 100)
if self.half_pie:
start_angle = 3*pi/2
center = (
(current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.),
(current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 4))
end_angle = pi / 2
else:
start_angle = 0
center = (
(current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 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])
value = serie.values[0]
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
maxvalue = serie.metadata.get(0, {}).get('maxvalue', 100)
radius = min([sq_dimensions[0]/2, sq_dimensions[1]/2]) * .9
small_radius = radius * serie.inner_radius
alter(self.svg.solidgauge(
serie_node, gauge_, big_radius, small_radius,
angle, startangle, center, value, 0, metadata,
self.half_pie, endangle, self._format(maxvalue)), metadata)
self.svg.gauge_background(
serie_node, start_angle, center, radius, small_radius, end_angle,
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):
pass
@ -81,15 +103,11 @@ class SolidGauge(Graph):
"""Draw all the serie slices"""
squares = self._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):
current_square = self._current_square(squares, index)
self.gaugify(
serie, startangle, squares, sq_dimensions, current_square)
serie, squares, sq_dimensions, current_square)
def _squares(self):

7
pygal/style.py

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

160
pygal/svg.py

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