Browse Source

Add render_sparkline shorthand function and some config to make it possible

pull/35/merge
Florian Mounier 11 years ago
parent
commit
d38727a259
  1. 9
      demo/moulinrouge/__init__.py
  2. 11
      demo/moulinrouge/static/css.css
  3. 11
      demo/moulinrouge/templates/index.jinja2
  4. 11
      pygal/config.py
  5. 32
      pygal/ghost.py
  6. 28
      pygal/graph/base.py
  7. 22
      pygal/graph/graph.py
  8. 49
      pygal/test/test_sparktext.py

9
demo/moulinrouge/__init__.py

@ -16,7 +16,7 @@
# #
# You should have received a copy of the GNU Lesser General Public License # You should have received a copy of the GNU Lesser General Public License
# along with pygal. If not, see <http://www.gnu.org/licenses/>. # along with pygal. If not, see <http://www.gnu.org/licenses/>.
from flask import Flask, render_template from flask import Flask, render_template, Response
import pygal import pygal
from pygal.config import Config from pygal.config import Config
from pygal.util import cut from pygal.util import cut
@ -94,6 +94,13 @@ def create_app():
graph.add(title, values) graph.add(title, values)
return graph.render_response() return graph.render_response()
@app.route("/sparkline/<style>")
def sparkline(style):
line = pygal.Line(style=styles[style])
line.add('_', [random.randrange(0, 10) for _ in range(25)])
return Response(
line.render_sparkline(height=40), mimetype='image/svg+xml')
@app.route("/all") @app.route("/all")
@app.route("/all/style=<style>") @app.route("/all/style=<style>")
@app.route("/all/interpolate=<interpolate>") @app.route("/all/interpolate=<interpolate>")

11
demo/moulinrouge/static/css.css

@ -3,7 +3,18 @@ html, body, section, figure {
padding: 0; padding: 0;
} }
li {
display: block;
overflow: auto;
font-size: 1.5em;
}
li a {
vertical-align: super;
}
figure { figure {
display: block;
float: left; float: left;
border: 1px solid #ccc; border: 1px solid #ccc;
} }

11
demo/moulinrouge/templates/index.jinja2

@ -1,12 +1,15 @@
{% extends '_layout.jinja2' %} {% extends '_layout.jinja2' %}
{% block section %} {% block section %}
<dl> <h1>Moulin Rouge</h1>
<dt>All types</dt> <ul>
{% for style in styles %} {% for style in styles %}
<dd><a href="{{ url_for('all', style=style) }}">{{ style }}</a></dd> <li>
<embed src="{{ url_for('sparkline', style=style) }}" type="image/svg+xml" />
<a href="{{ url_for('all', style=style) }}">{{ style | title }}</a>
</li>
{% endfor %} {% endfor %}
</dl> </ul>
<a href="{{ url_for('interpolation') }}">Interpolation</a> <a href="{{ url_for('interpolation') }}">Interpolation</a>
<a href="{{ url_for('rotation') }}">Rotations test</a> <a href="{{ url_for('rotation') }}">Rotations test</a>
<hr /> <hr />

11
pygal/config.py

@ -141,6 +141,14 @@ class Config(object):
rounded_bars = Key( rounded_bars = Key(
None, int, "Look", "Set this to the desired radius in px") None, int, "Look", "Set this to the desired radius in px")
spacing = Key(
10, int, "Look",
"Space between titles/legend/axes")
margin = Key(
20, int, "Look",
"Margin around chart")
############ Label ############ ############ Label ############
x_labels = Key( x_labels = Key(
None, list, "Label", None, list, "Label",
@ -169,6 +177,9 @@ class Config(object):
"You can specify explicit y labels", "You can specify explicit y labels",
"Must be a list of numbers", float) "Must be a list of numbers", float)
show_y_labels = Key(
True, bool, "Label", "Set to false to hide y-labels")
x_label_rotation = Key( x_label_rotation = Key(
0, int, "Label", "Specify x labels rotation angles", "in degrees") 0, int, "Label", "Specify x labels rotation angles", "in degrees")

32
pygal/ghost.py

@ -26,6 +26,7 @@ It is used to delegate rendering to real objects but keeping config in place
import io import io
import sys import sys
from pygal.config import Config from pygal.config import Config
from pygal._compat import u
from pygal.graph import CHARTS_NAMES from pygal.graph import CHARTS_NAMES
from pygal.util import prepare_values from pygal.util import prepare_values
@ -69,8 +70,9 @@ class Ghost(object):
def make_series(self, series): def make_series(self, series):
return prepare_values(series, self.config, self.cls) return prepare_values(series, self.config, self.cls)
def make_instance(self): def make_instance(self, overrides={}):
self.config(**self.__dict__) self.config(**self.__dict__)
self.config.__dict__.update(overrides)
series = self.make_series(self.raw_series) series = self.make_series(self.raw_series)
secondary_series = self.make_series(self.raw_series2) secondary_series = self.make_series(self.raw_series2)
self._last__inst = self.cls(self.config, series, secondary_series) self._last__inst = self.cls(self.config, series, secondary_series)
@ -109,6 +111,34 @@ class Ghost(object):
return cairosvg.svg2png( return cairosvg.svg2png(
bytestring=self.render(), write_to=filename, dpi=dpi) bytestring=self.render(), write_to=filename, dpi=dpi)
def render_sparktext(self):
"""Make a mini text sparkline from chart"""
bars = u('▁▂▃▄▅▆▇█')
if len(self.raw_series) == 0:
return u('')
values = list(self.raw_series[0][1])
if len(values) == 0:
return u('')
chart = u('')
vmax = max(values)
divisions = len(bars)
for value in values:
chart += bars[int(divisions * min(value, 0) / vmax)]
return chart
def render_sparkline(self, width=200, height=50):
return self.make_instance(dict(
width=width,
height=height,
show_dots=False,
show_legend=False,
show_y_labels=False,
spacing=0,
margin=5,
explicit_size=True
)).render()
def _repr_png_(self): def _repr_png_(self):
"""Display png in IPython notebook""" """Display png in IPython notebook"""
return self.render_to_png() return self.render_to_png()

28
pygal/graph/base.py

@ -47,7 +47,7 @@ class BaseGraph(object):
self._x_2nd_labels = None self._x_2nd_labels = None
self._y_2nd_labels = None self._y_2nd_labels = None
self.nodes = {} self.nodes = {}
self.margin = Margin(*([20] * 4)) self.margin = Margin(*([self.margin] * 4))
self._box = Box() self._box = Box()
self.view = None self.view = None
if self.logarithmic and self.zero == 0: if self.logarithmic and self.zero == 0:
@ -100,15 +100,16 @@ class BaseGraph(object):
self.legend_font_size) self.legend_font_size)
if self.legend_at_bottom: if self.legend_at_bottom:
h_max = max(h, self.legend_box_size) h_max = max(h, self.legend_box_size)
self.margin.bottom += 10 + h_max * round( self.margin.bottom += self.spacing + h_max * round(
sqrt(self._order) - 1) * 1.5 + h_max sqrt(self._order) - 1) * 1.5 + h_max
else: else:
if series_group is self.series: if series_group is self.series:
legend_width = 10 + w + self.legend_box_size legend_width = self.spacing + w + self.legend_box_size
self.margin.left += legend_width self.margin.left += legend_width
self._legend_at_left_width += legend_width self._legend_at_left_width += legend_width
else: else:
self.margin.right += 10 + w + self.legend_box_size self.margin.right += (
self.spacing + w + self.legend_box_size)
for xlabels in (self._x_labels, self._x_2nd_labels): for xlabels in (self._x_labels, self._x_2nd_labels):
if xlabels: if xlabels:
@ -116,7 +117,7 @@ class BaseGraph(object):
map(lambda x: truncate(x, self.truncate_label or 25), map(lambda x: truncate(x, self.truncate_label or 25),
cut(xlabels)), cut(xlabels)),
self.label_font_size) self.label_font_size)
self._x_labels_height = 10 + max( self._x_labels_height = self.spacing + max(
w * sin(rad(self.x_label_rotation)), h) w * sin(rad(self.x_label_rotation)), h)
if xlabels is self._x_labels: if xlabels is self._x_labels:
self.margin.bottom += self._x_labels_height self.margin.bottom += self._x_labels_height
@ -129,15 +130,16 @@ class BaseGraph(object):
if not self._x_labels: if not self._x_labels:
self._x_labels_height = 0 self._x_labels_height = 0
if self.show_y_labels:
for ylabels in (self._y_labels, self._y_2nd_labels): for ylabels in (self._y_labels, self._y_2nd_labels):
if ylabels: if ylabels:
h, w = get_texts_box( h, w = get_texts_box(
cut(ylabels), self.label_font_size) cut(ylabels), self.label_font_size)
if ylabels is self._y_labels: if ylabels is self._y_labels:
self.margin.left += 10 + max( self.margin.left += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h) w * cos(rad(self.y_label_rotation)), h)
else: else:
self.margin.right += 10 + max( self.margin.right += self.spacing + max(
w * cos(rad(self.y_label_rotation)), h) w * cos(rad(self.y_label_rotation)), h)
self.title = split_title( self.title = split_title(
@ -145,7 +147,7 @@ class BaseGraph(object):
if self.title: if self.title:
h, _ = get_text_box(self.title[0], self.title_font_size) h, _ = get_text_box(self.title[0], self.title_font_size)
self.margin.top += len(self.title) * (10 + h) self.margin.top += len(self.title) * (self.spacing + h)
self.x_title = split_title( self.x_title = split_title(
self.x_title, self.width - self.margin.x, self.title_font_size) self.x_title, self.width - self.margin.x, self.title_font_size)
@ -153,9 +155,9 @@ class BaseGraph(object):
self._x_title_height = 0 self._x_title_height = 0
if self.x_title: if self.x_title:
h, _ = get_text_box(self.x_title[0], self.title_font_size) h, _ = get_text_box(self.x_title[0], self.title_font_size)
height = len(self.x_title) * (10 + h) height = len(self.x_title) * (self.spacing + h)
self.margin.bottom += height self.margin.bottom += height
self._x_title_height = height + 10 self._x_title_height = height + self.spacing
self.y_title = split_title( self.y_title = split_title(
self.y_title, self.height - self.margin.y, self.title_font_size) self.y_title, self.height - self.margin.y, self.title_font_size)
@ -163,9 +165,9 @@ class BaseGraph(object):
self._y_title_height = 0 self._y_title_height = 0
if self.y_title: if self.y_title:
h, _ = get_text_box(self.y_title[0], self.title_font_size) h, _ = get_text_box(self.y_title[0], self.title_font_size)
height = len(self.y_title) * (10 + h) height = len(self.y_title) * (self.spacing + h)
self.margin.left += height self.margin.left += height
self._y_title_height = height + 10 self._y_title_height = height + self.spacing
@cached_property @cached_property
def _legends(self): def _legends(self):
@ -247,7 +249,7 @@ class BaseGraph(object):
map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( map(len, map(lambda s: s.safe_values, self.series))) != 0 and (
sum(map(abs, self._values)) != 0) sum(map(abs, self._values)) != 0)
def render(self, is_unicode): def render(self, is_unicode=False):
"""Render the graph, and return the svg string""" """Render the graph, and return the svg string"""
return self.svg.render( return self.svg.render(
is_unicode=is_unicode, pretty_print=self.pretty_print) is_unicode=is_unicode, pretty_print=self.pretty_print)

22
pygal/graph/graph.py

@ -206,7 +206,7 @@ class Graph(BaseGraph):
def _y_axis(self, draw_axes=True): def _y_axis(self, draw_axes=True):
"""Make the y axis: labels and guides""" """Make the y axis: labels and guides"""
if not self._y_labels: if not self._y_labels or not self.show_y_labels:
return return
axis = self.svg.node(self.nodes['plot'], class_="axis y") axis = self.svg.node(self.nodes['plot'], class_="axis y")
@ -270,10 +270,10 @@ class Graph(BaseGraph):
return return
truncation = self.truncate_legend truncation = self.truncate_legend
if self.legend_at_bottom: if self.legend_at_bottom:
x = self.margin.left + 10 x = self.margin.left + self.spacing
y = (self.margin.top + self.view.height + y = (self.margin.top + self.view.height +
self._x_title_height + self._x_title_height +
self._x_labels_height + 10) self._x_labels_height + self.spacing)
cols = ceil(sqrt(self._order)) or 1 cols = ceil(sqrt(self._order)) or 1
if not truncation: if not truncation:
@ -282,8 +282,8 @@ class Graph(BaseGraph):
truncation = reverse_text_len( truncation = reverse_text_len(
available_space, self.legend_font_size) available_space, self.legend_font_size)
else: else:
x = 10 x = self.spacing
y = self.margin.top + 10 y = self.margin.top + self.spacing
cols = 1 cols = 1
if not truncation: if not truncation:
truncation = 15 truncation = 15
@ -311,13 +311,13 @@ class Graph(BaseGraph):
enumerate(zip(self._secondary_legends, repeat(True))))) enumerate(zip(self._secondary_legends, repeat(True)))))
# draw secondary axis on right # draw secondary axis on right
x = self.margin.left + self.view.width + 10 x = self.margin.left + self.view.width + self.spacing
if self._y_2nd_labels: if self._y_2nd_labels:
h, w = get_texts_box( h, w = get_texts_box(
cut(self._y_2nd_labels), self.label_font_size) cut(self._y_2nd_labels), self.label_font_size)
x += 10 + max(w * cos(rad(self.y_label_rotation)), h) x += self.spacing + max(w * cos(rad(self.y_label_rotation)), h)
y = self.margin.top + 10 y = self.margin.top + self.spacing
secondary_legends = self.svg.node( secondary_legends = self.svg.node(
self.nodes['graph'], class_='legends', self.nodes['graph'], class_='legends',
@ -362,7 +362,7 @@ class Graph(BaseGraph):
self.svg.node( self.svg.node(
self.nodes['title'], 'text', class_='title', self.nodes['title'], 'text', class_='title',
x=self.width / 2, x=self.width / 2,
y=i * (self.title_font_size + 10) y=i * (self.title_font_size + self.spacing)
).text = title_line ).text = title_line
def _x_title(self): def _x_title(self):
@ -374,7 +374,7 @@ class Graph(BaseGraph):
text = self.svg.node( text = self.svg.node(
self.nodes['title'], 'text', class_='title', self.nodes['title'], 'text', class_='title',
x=self.margin.left + self.view.width / 2, x=self.margin.left + self.view.width / 2,
y=y + i * (self.title_font_size + 10) y=y + i * (self.title_font_size + self.spacing)
) )
text.text = title_line text.text = title_line
@ -386,7 +386,7 @@ class Graph(BaseGraph):
text = self.svg.node( text = self.svg.node(
self.nodes['title'], 'text', class_='title', self.nodes['title'], 'text', class_='title',
x=self._legend_at_left_width, x=self._legend_at_left_width,
y=i * (self.title_font_size + 10) + yc y=i * (self.title_font_size + self.spacing) + yc
) )
text.attrib['transform'] = "rotate(%d %f %f)" % ( text.attrib['transform'] = "rotate(%d %f %f)" % (
-90, self._legend_at_left_width, yc) -90, self._legend_at_left_width, yc)

49
pygal/test/test_sparktext.py

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
# This file is part of pygal
#
# A python svg graph plotting library
# Copyright © 2012-2013 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 <http://www.gnu.org/licenses/>.
from pygal import Line, Bar
from pygal._compat import u
def test_basic_sparktext():
chart = Line()
chart.add('_', [1, 5, 22, 13, 53])
chart.render_sparktext() == u('▁▁▃▂▇')
def test_another_sparktext():
chart = Line()
chart.add('_', [0, 30, 55, 80, 33, 150])
chart.render_sparktext() == u('▁▂▃▅▂▇')
chart.render_sparktext() == chart.render_sparktext()
chart2 = Bar()
chart2.add('_', [0, 30, 55, 80, 33, 150])
chart2.render_sparktext() == chart.render_sparktext()
def test_negative_and_float_and_no_data_sparktext():
chart = Line()
chart.add('_', [0.1, 0.2, 0.9, -0.5])
chart.render_sparktext() == u('▄▅█▁')
chart2 = Line()
chart2.add('_', [])
chart2.render_sparktext() == u('')
chart3 = Line()
chart3.render_sparktext() == u('')
Loading…
Cancel
Save