diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index cebf932..2dfddcb 100644 --- a/demo/moulinrouge/tests.py +++ b/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() diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 3287a77..a638fb7 100644 --- a/pygal/css/graph.css +++ b/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; } diff --git a/pygal/graph/solidgauge.py b/pygal/graph/solidgauge.py index e55488b..09a690f 100644 --- a/pygal/graph/solidgauge.py +++ b/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): diff --git a/pygal/style.py b/pygal/style.py index 79bda69..c22395d 100644 --- a/pygal/style.py +++ b/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""" diff --git a/pygal/svg.py b/pygal/svg.py index dd7350d..97b49f5 100644 --- a/pygal/svg.py +++ b/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]) diff --git a/pygal/util.py b/pygal/util.py index 01b454c..a841627 100644 --- a/pygal/util.py +++ b/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)))