From 6ede0d580b8d3d0bd1ed8dca76bcf63abd87ee7b Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 19 Apr 2012 14:09:55 +0200 Subject: [PATCH] Add gauge --- demo/simple_test.py | 8 +++ pygal/__init__.py | 6 ++- pygal/config.py | 2 + pygal/css/graph.css | 1 + pygal/graph/base.py | 4 +- pygal/graph/gauge.py | 122 +++++++++++++++++++++++++++++++++++++++++++ pygal/graph/pie.py | 13 +++-- 7 files changed, 147 insertions(+), 9 deletions(-) create mode 100644 pygal/graph/gauge.py diff --git a/demo/simple_test.py b/demo/simple_test.py index 038205a..d4c6ab7 100755 --- a/demo/simple_test.py +++ b/demo/simple_test.py @@ -26,6 +26,13 @@ lnk = lambda v, l=None: {'value': v, 'xlink': 'javascript:alert("Test %s")' % v, t_start = time.time() +gauge = Gauge() + +gauge.range = [-10, 10] +gauge.add('Need l', [2.3, 5.12]) +# gauge.add('No', [99, -99]) +gauge.render_to_file('out-gauge.svg') + pyramid = Pyramid() pyramid.x_labels = ['0-25', '25-45', '45-65', '65+'] @@ -168,6 +175,7 @@ pie.add('test3', [24, 10, 32]) pie.add('test4', [20, lnk(18), 9]) pie.add('test5', [17, 5, 10]) pie.add('test6', [None, None, 10]) + # pie.included_js = [] # pie.external_js = [ # 'http://localhost:7575/svg.jquery.js', diff --git a/pygal/__init__.py b/pygal/__init__.py index 792b4b7..9023aaa 100644 --- a/pygal/__init__.py +++ b/pygal/__init__.py @@ -26,11 +26,12 @@ __version__ = '0.9.22' from pygal.config import Config from pygal.graph.bar import Bar from pygal.graph.dot import Dot +from pygal.graph.funnel import Funnel +from pygal.graph.gauge import Gauge from pygal.graph.horizontal import HorizontalBar from pygal.graph.horizontal import HorizontalStackedBar from pygal.graph.line import Line from pygal.graph.pie import Pie -from pygal.graph.funnel import Funnel from pygal.graph.pyramid import Pyramid from pygal.graph.radar import Radar from pygal.graph.stackedbar import StackedBar @@ -41,8 +42,9 @@ from pygal.graph.xy import XY #: List of all chart types CHARTS = [ Bar, - Funnel, Dot, + Funnel, + Gauge, HorizontalBar, HorizontalStackedBar, Line, diff --git a/pygal/config.py b/pygal/config.py index bf8923d..ba130dc 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -87,6 +87,8 @@ class Config(object): interpolate = None #: Number of interpolated points between two values interpolation_precision = 250 + #: Explicitly specify min and max of values (ie: (0, 100)) + range = None #: Set the ordinate zero value (for filling) zero = 0 #: Text to display when no data is given diff --git a/pygal/css/graph.css b/pygal/css/graph.css index d5174aa..7b7f5c4 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -72,6 +72,7 @@ text.no_data { .axis.y .guides:hover .guide.line, .line-graph .axis.x .guides:hover .guide.line, +.gauge-graph .axis.x .guides:hover .guide.line, .stackedline-graph .axis.x .guides:hover .guide.line, .xy-graph .axis.x .guides:hover .guide.line { opacity: 1; diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 577192e..02313fb 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -128,12 +128,12 @@ class BaseGraph(object): @cached_property def _min(self): """Getter for the minimum series value""" - return min(self._values) + return (self.range and self.range[0]) or min(self._values) @cached_property def _max(self): """Getter for the maximum series value""" - return max(self._values) + return (self.range and self.range[1]) or max(self._values) def _draw(self): """Draw all the things""" diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py new file mode 100644 index 0000000..de5f8e5 --- /dev/null +++ b/pygal/graph/gauge.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012 Kozea +# +# This library is free software: you can redistribute it and/or modify it under +# the terms of the GNU Lesser General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This library is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more +# details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with pygal. If not, see . +""" +Gauge chart + +""" + +from __future__ import division +from pygal.util import decorate, compute_scale +from pygal.view import PolarView +from pygal.graph.graph import Graph +from math import pi + + +class Gauge(Graph): + """Gauge graph""" + + def _set_view(self): + self.view = PolarView( + self.width - self.margin.x, + self.height - self.margin.y, + self._box) + + def arc_pos(self, value): + aperture = pi / 3 + if value > self._max: + return (3 * pi - aperture / 2) / 2 + if value < self._min: + return (3 * pi + aperture / 2) / 2 + start = 3 * pi / 2 + aperture / 2 + return start + (2 * pi - aperture) * ( + value - self.min_) / (self.max_ - self.min_) + + def needle(self, serie_node, serie,): + thickness = .05 + for i, value in enumerate(serie.values): + theta = self.arc_pos(value) + fmt = lambda x: '%f %f' % x + value = self._format(serie.values[i]) + metadata = serie.metadata[i] + gauges = decorate( + self.svg, + self.svg.node(serie_node['plot'], class_="dots"), + metadata) + + self.svg.node(gauges, 'polygon', points=' '.join([ + fmt(self.view((0, 0))), + fmt(self.view((.75, theta + thickness))), + fmt(self.view((.8, theta))), + fmt(self.view((.75, theta - thickness)))]), + class_='line reactive tooltip-trigger') + + x, y = self.view((.75, theta)) + self._tooltip_data(gauges, value, x, y) + self._static_value(serie_node, value, x, y) + + def _x_axis(self, draw_axes=True): + if not self._x_labels: + return + + axis = self.svg.node(self.nodes['plot'], class_="axis x gauge") + + for i, (label, pos) in enumerate(self._x_labels): + guides = self.svg.node(axis, class_='guides') + theta = self.arc_pos(pos) + self.svg.line( + guides, [self.view((.95, theta)), self.view((1, theta))], + close=True, + class_='line') + + self.svg.line( + guides, [self.view((0, theta)), self.view((.95, theta))], + close=True, + class_='guide line %s' % ('major' + if i in (0, len(self._x_labels) - 1) else '')) + + x, y = self.view((.9, theta)) + self.svg.node(guides, 'text', + x=x, + y=y + ).text = label + + def _y_axis(self, draw_axes=True): + axis = self.svg.node(self.nodes['plot'], class_="axis y gauge") + x, y = self.view((0, 0)) + self.svg.node(axis, 'circle', cx=x, cy=y, r=4) + + def _compute(self): + self._box.xmin = -1 + self._box.ymin = -1 + + self.min_ = self._min + self.max_ = self._max + if self.max_ - self.min_ == 0: + self.min_ -= 1 + self.max_ += 1 + + x_pos = compute_scale( + self.min_, self.max_, self.logarithmic + ) + self._x_labels = zip(map(self._format, x_pos), x_pos) + + def _plot(self): + for serie in self.series: + self.needle( + self._serie(serie.index), serie) diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index 9b6f301..3e7f2f9 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -35,6 +35,7 @@ class Pie(Graph): def slice(self, serie_node, start_angle, serie, total): """Make a serie slice""" + dual = self._len > 1 and not len(self.series) == 1 slices = self.svg.node(serie_node['plot'], class_="slices") serie_angle = 0 @@ -53,18 +54,20 @@ class Pie(Graph): self.svg, self.svg.node(slices, class_="slice"), metadata) - if len(serie.values) > 1: + if dual: small_radius = radius * .9 + big_radius = radius else: - radius = radius * .9 + big_radius = radius * .9 small_radius = 0 - self.svg.slice(serie_node, - slice_, radius, small_radius, angle, start_angle, center, val) + self.svg.slice( + serie_node, slice_, big_radius, small_radius, + angle, start_angle, center, val) start_angle += angle total_perc += perc - if len(serie.values) > 1: + if dual: val = '{0:.2%}'.format(total_perc) self.svg.slice(serie_node, self.svg.node(slices, class_="big_slice"),