Browse Source

solidgauge

pull/295/head
chartique 9 years ago
parent
commit
efd5791cc6
  1. 17
      demo/moulinrouge/tests.py
  2. 1
      pygal/__init__.py
  3. 10
      pygal/css/graph.css
  4. 29
      pygal/graph/graph.py
  5. 126
      pygal/graph/solidgauge.py
  6. 3
      pygal/style.py
  7. 112
      pygal/svg.py

17
demo/moulinrouge/tests.py

@ -202,6 +202,23 @@ def get_test_routes(app):
]
return gauge.render_response()
@app.route('/test/solidguage/')
def test_solidguage():
gauge = SolidGauge(half_pie=True,
inner_radius=0.70,
print_values=True,
human_readable=True)
gauge.title = 'Hello World!'
gauge.value_formatter = lambda x: '${}'.format(x.values[0]) if x.title in ['Series 1'] else '{}%'.format(x.values[0])
gauge.add('Series 1', [{'value': 225000, 'maxvalue': 1275000}])
gauge.add('Series 2', [{'value': 81, 'maxvalue': 100}])
gauge.add('Series 3', [{'value': 3, 'maxvalue': 100}])
gauge.add('Series 4', [{'value': 51, 'maxvalue': 100}])
gauge.add('Series 5', [{'value': 79, 'maxvalue': 100}])
gauge.add('Series 6', [{'value': 99, 'maxvalue': 100}])
gauge.add('Series 7', [{'value': 100, 'maxvalue': 100}])
return gauge.render_response()
@app.route('/test/gauge/log')
def test_gauge_log():
gauge = Gauge(logarithmic=True)

1
pygal/__init__.py

@ -36,6 +36,7 @@ from pygal.graph.box import Box
from pygal.graph.dot import Dot
from pygal.graph.funnel import Funnel
from pygal.graph.gauge import Gauge
from pygal.graph.solidgauge import SolidGauge
from pygal.graph.histogram import Histogram
from pygal.graph.horizontalbar import HorizontalBar
from pygal.graph.horizontalstackedbar import HorizontalStackedBar

10
pygal/css/graph.css

@ -140,3 +140,13 @@
{{ id }}.showable.shown {
visibility: visible;
}
{{ id }}.background-shade {
fill: {{ style.gauge_background_color }};
stroke: none;
}
{{ id }}.bg-lines {
stroke: {{ style.square_border_color }};
stroke-width: {{ style.square_border_width }};
}

29
pygal/graph/graph.py

@ -492,7 +492,8 @@ class Graph(PublicApi):
self.svg.node(node, 'desc',
class_="x_label").text = to_str(xlabel)
def _static_value(self, serie_node, value, x, y, metadata, classes=None):
def _static_value(self, serie_node, value, x, y, metadata,
align_text='left', classes=None):
"""Write the print value"""
label = metadata and metadata.get('label')
classes = classes and [classes] or []
@ -518,7 +519,8 @@ class Graph(PublicApi):
serie_node['text_overlay'], 'text',
class_=' '.join(val_cls),
x=x,
y=y + self.style.value_font_size / 3
y=y + self.style.value_font_size / 3,
attrib={'text-anchor': align_text}
).text = value if self.print_zeroes or value != '0' else ''
def _get_value(self, values, i):
@ -850,6 +852,29 @@ class Graph(PublicApi):
else:
self._y_labels_major = []
def add_squares(self, squares):
x_lines = squares[0]-1
y_lines = squares[1]-1
_current_x = 0
_current_y = 0
_squares_coord = []
for line in range(x_lines):
_current_x += (self.width - self.margin_box.x) / squares[0]
self.svg.node(self.nodes['plot'], 'path',
class_='bg-lines',
d='M%s %s L%s %s' % (_current_x, 0, _current_x, self.height-self.margin_box.y))
for line in range(y_lines):
_current_y += (self.height - self.margin_box.y) / squares[1]
self.svg.node(self.nodes['plot'], 'path',
class_='bg-lines',
d='M%s %s L%s %s' % (0, _current_y, self.width-self.margin_box.x, _current_y))
return ((self.width - self.margin_box.x) / squares[0], (self.height - self.margin_box.y) / squares[1])
def _draw(self):
"""Draw all the things"""
self._compute()

126
pygal/graph/solidgauge.py

@ -0,0 +1,126 @@
from math import pi, sqrt, sin, cos
from pygal.graph.graph import Graph
from pygal.util import alter, decorate
class SolidGauge(Graph):
"""
Solid Guage
For each series a solid guage is shown on the plot area.
See http://en.wikipedia.org/wiki/#####
"""
def gaugify(self, serie, startangle, squares, sq_dimensions, current_square):
serie_node = self.svg.serie(serie)
metadata = serie.metadata.get(0)
if 'maxvalue' in metadata.keys():
maxvalue = metadata['maxvalue']
else:
maxvalue = serie.values[0] * 1.5
if self.half_pie:
center = ((current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.),
(current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 4))
else:
center = ((current_square[1]*sq_dimensions[0]) - (sq_dimensions[0] / 2.),
(current_square[0]*sq_dimensions[1]) - (sq_dimensions[1] / 2.))
radius = min([sq_dimensions[0]/2, sq_dimensions[1]/2])
value = serie.values[0]
ratio = value / maxvalue
if self.half_pie:
angle = 2 * pi * ratio / 2
endangle = pi / 2
else:
angle = 2 * pi * ratio
endangle = 2 * pi
value = self._format(serie)
gauge_ = decorate(
self.svg,
self.svg.node(serie_node['plot'], class_="gauge"),
metadata)
big_radius = radius * .9
small_radius = radius * serie.inner_radius
maxvalue = Maxvalue(serie)
print(maxvalue.values)
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)
def _compute_x_labels(self):
pass
def _compute_y_labels(self):
pass
def _plot(self):
"""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)
def _squares(self):
n_series_ = len(self.series)
i = 2
if sqrt(n_series_).is_integer():
_x = int(sqrt(n_series_))
_y = int(sqrt(n_series_))
else:
while i * i < n_series_:
while n_series_ % i == 0:
n_series_ = n_series_ / i
i = i + 1
_y = int(n_series_)
_x = int(n_series_ / len(self.series))
if len(self.series) == 5:
_x, _y = 2, 3
if abs(_x - _y) > 2:
_sq = 3
while (_x * _y)-1 < len(self.series):
_x, _y = _sq, _sq
_sq += 1
return (_x, _y)
def _current_square(self, squares, index):
current_square = [1, 1]
steps = index + 1
steps_taken = 0
for i in range(squares[0] * squares[1]):
steps_taken += 1
if steps_taken != steps and steps_taken % squares[0] != 0:
current_square[1] += 1
elif steps_taken != steps and steps_taken % squares[0] == 0:
current_square[1] = 1
current_square[0] += 1
else:
return tuple(current_square)
return print('Something went wrong with the current square assignment.')
class Maxvalue(SolidGauge):
def __init__(self, serie):
self.serie = serie
@property
def values(self):
return self.serie.values
@property
def title(self):
return self.serie.title

3
pygal/style.py

@ -89,6 +89,9 @@ class Style(object):
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"""

112
pygal/svg.py

@ -296,6 +296,118 @@ class Svg(object):
self.graph._static_value(serie_node, val, x, y, metadata)
return rv
def solidgauge(
self, serie_node, node, radius, small_radius,
angle, start_angle, center, val, i, metadata, half_pie, endangle, 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)]
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')
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)]
if half_pie:
begin_end = [
diff(center,
project(radius-(radius-small_radius)/2, start_angle)),
diff(center,
project(radius-(radius-small_radius)/2, endangle))]
pos = 0
for i in begin_end:
self.node(
node, 'text',
class_='y-{} reactive'.format(pos),
x=i[0],
y=i[1]+10,
attrib={'text-anchor': 'middle',
'font-size': 10}
).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')
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(
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')
else:
return
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))
self.graph._tooltip_data(
node, val, x, y, "centered",
self.graph._x_labels and self.graph._x_labels[i][0])
def confidence_interval(self, node, x, low, high, width=7):
if self.graph.horizontal:
coord_format = lambda xy: '%f %f' % (xy[1], xy[0])

Loading…
Cancel
Save