Browse Source

Pylint and interpolation refactoring

pull/8/head
Florian Mounier 13 years ago
parent
commit
a1ec68ef8e
  1. 4
      .pylintrc
  2. 4
      demo/moulinrouge/__init__.py
  3. 1
      pygal/config.py
  4. 4
      pygal/graph/__init__.py
  5. 26
      pygal/graph/bar.py
  6. 88
      pygal/graph/base.py
  7. 74
      pygal/graph/graph.py
  8. 9
      pygal/graph/horizontal.py
  9. 31
      pygal/graph/line.py
  10. 24
      pygal/graph/pie.py
  11. 58
      pygal/graph/radar.py
  12. 18
      pygal/graph/stackedbar.py
  13. 25
      pygal/graph/stackedline.py
  14. 34
      pygal/graph/xy.py
  15. 4
      pygal/svg.py
  16. 70
      pygal/util.py
  17. 6
      pygal/view.py

4
.pylintrc

@ -133,7 +133,7 @@ module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$ const-rgx=(([A-Za-z_][A-Za-z0-9_]*)|(__.*__))$
# Regular expression which should only match correct class names # Regular expression which should only match correct class names
class-rgx=[a-zA-Z_][a-zA-Z0-9]+$ class-rgx=[a-zA-Z_][a-zA-Z0-9_]+$
# Regular expression which should only match correct function names # Regular expression which should only match correct function names
function-rgx=[a-z_][a-z0-9_]{2,30}$ function-rgx=[a-z_][a-z0-9_]{2,30}$
@ -200,7 +200,7 @@ max-args=20
ignored-argument-names=_.* ignored-argument-names=_.*
# Maximum number of locals for function / method body # Maximum number of locals for function / method body
max-locals=15 max-locals=30
# Maximum number of return / yield for function / method body # Maximum number of return / yield for function / method body
max-returns=6 max-returns=6

4
demo/moulinrouge/__init__.py

@ -103,7 +103,8 @@ def create_app():
@app.route("/all") @app.route("/all")
@app.route("/all/style=<style>") @app.route("/all/style=<style>")
def all(style=DefaultStyle): @app.route("/all/interpolate=<interpolate>")
def all(style='default', interpolate=None):
width, height = 600, 400 width, height = 600, 400
data = random.randrange(1, 10) data = random.randrange(1, 10)
order = random.randrange(1, 10) order = random.randrange(1, 10)
@ -119,6 +120,7 @@ def create_app():
config.height = height config.height = height
config.fill = bool(random.randrange(0, 2)) config.fill = bool(random.randrange(0, 2))
config.human_readable = True config.human_readable = True
config.interpolate = interpolate
config.style = styles[style] config.style = styles[style]
config.x_labels = [random_label() for i in range(data)] config.x_labels = [random_label() for i in range(data)]
svgs = [] svgs = []

1
pygal/config.py

@ -31,7 +31,6 @@ class FontSizes(object):
class Config(object): class Config(object):
"""Class holding config values""" """Class holding config values"""
_horizontal = False
#: Graph width #: Graph width
width = 800 width = 800

4
pygal/graph/__init__.py

@ -16,3 +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/>.
"""
Graph modules
"""

26
pygal/graph/bar.py

@ -16,14 +16,23 @@
# #
# 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/>.
"""
Bar chart
"""
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.util import swap, ident from pygal.util import swap, ident, compute_scale
class Bar(Graph): class Bar(Graph):
"""Bar graph""" """Bar graph"""
def __init__(self, *args, **kwargs):
self._x_ranges = None
super(Bar, self).__init__(*args, **kwargs)
def bar(self, serie_node, serie, values, stack_vals=None): def bar(self, serie_node, serie, values, stack_vals=None):
"""Draw a bar graph for a serie""" """Draw a bar graph for a serie"""
# value here is a list of tuple range of tuple coord # value here is a list of tuple range of tuple coord
@ -31,7 +40,7 @@ class Bar(Graph):
def view(rng): def view(rng):
"""Project range""" """Project range"""
t, T = rng t, T = rng
fun = swap if self._horizontal else ident fun = swap if self.horizontal else ident
return self.view(fun(t)), self.view(fun(T)) return self.view(fun(t)), self.view(fun(T))
bars = self.svg.node(serie_node['plot'], class_="bars") bars = self.svg.node(serie_node['plot'], class_="bars")
@ -52,12 +61,12 @@ class Bar(Graph):
# #
# x and y are left range coords and X, Y right ones # x and y are left range coords and X, Y right ones
val = self._format(values[i][1][1]) val = self._format(values[i][1][1])
if self._horizontal: if self.horizontal:
x, y, X, Y = Y, X, y, x x, y, X, Y = Y, X, y, x
width = X - x width = X - x
padding = .1 * width padding = .1 * width
inner_width = width - 2 * padding inner_width = width - 2 * padding
if self._horizontal: if self.horizontal:
height = self.view.x(0) - y height = self.view.x(0) - y
else: else:
height = self.view.y(0) - y height = self.view.y(0) - y
@ -93,11 +102,11 @@ class Bar(Graph):
str, (x + bar_inner_width / 2, y + height / 2)) str, (x + bar_inner_width / 2, y + height / 2))
self.svg.node(bar, 'desc', self.svg.node(bar, 'desc',
class_="x centered" class_="x centered"
).text = tooltip_positions[int(self._horizontal)] ).text = tooltip_positions[int(self.horizontal)]
self.svg.node(bar, 'desc', self.svg.node(bar, 'desc',
class_="y centered" class_="y centered"
).text = tooltip_positions[int(not self._horizontal)] ).text = tooltip_positions[int(not self.horizontal)]
if self._horizontal: if self.horizontal:
x += .3 * self.value_font_size x += .3 * self.value_font_size
y += height / 2 y += height / 2
else: else:
@ -117,7 +126,8 @@ class Bar(Graph):
x_step = len(self.series[0].values) x_step = len(self.series[0].values)
x_pos = [x / x_step for x in range(x_step + 1) x_pos = [x / x_step for x in range(x_step + 1)
] if x_step > 1 else [0, 1] # Center if only one value ] if x_step > 1 else [0, 1] # Center if only one value
y_pos = self._compute_scale(self._box.ymin, self._box.ymax, y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else map(float, self.y_labels)
self._x_ranges = zip(x_pos, x_pos[1:]) self._x_ranges = zip(x_pos, x_pos[1:])

88
pygal/graph/base.py

@ -16,15 +16,20 @@
# #
# 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/>.
"""
Base for pygal charts
"""
from __future__ import division from __future__ import division
import io import io
from pygal.serie import Serie from pygal.serie import Serie
from pygal.view import Margin, Box from pygal.view import Margin, Box
from pygal.util import round_to_scale, cut, rad, humanize from pygal.util import get_text_box, get_texts_box, cut, rad, humanize
from pygal.svg import Svg from pygal.svg import Svg
from pygal.config import Config from pygal.config import Config
from pygal.util import cached_property from pygal.util import cached_property
from math import log10, sin, cos, floor, ceil from math import sin, cos
class BaseGraph(object): class BaseGraph(object):
@ -32,11 +37,17 @@ class BaseGraph(object):
def __init__(self, config=None, **kwargs): def __init__(self, config=None, **kwargs):
"""Init the graph""" """Init the graph"""
self.horizontal = hasattr(self, 'horizontal') and self.horizontal
self.config = config or Config() self.config = config or Config()
self.config(**kwargs) self.config(**kwargs)
self.svg = Svg(self) self.svg = Svg(self)
self.series = [] self.series = []
self._x_labels = self._y_labels = None self._x_labels = None
self._y_labels = None
self._box = None
self.nodes = {}
self.margin = None
self.view = None
def add(self, title, values): def add(self, title, values):
"""Add a serie to this graph""" """Add a serie to this graph"""
@ -58,69 +69,6 @@ class BaseGraph(object):
"""Return the value formatter for this graph""" """Return the value formatter for this graph"""
return humanize if self.human_readable else str return humanize if self.human_readable else str
def _compute_logarithmic_scale(self, min_, max_):
"""Compute an optimal scale for logarithmic"""
min_order = int(floor(log10(min_)))
max_order = int(ceil(log10(max_)))
positions = []
amplitude = max_order - min_order
if amplitude <= 1:
return []
detail = 10.
while amplitude * detail < 20:
detail *= 2
while amplitude * detail > 50:
detail /= 2
for order in range(min_order, max_order + 1):
for i in range(int(detail)):
tick = (10 * i / detail or 1) * 10 ** order
tick = round_to_scale(tick, tick)
if min_ <= tick <= max_ and tick not in positions:
positions.append(tick)
return positions
def _compute_scale(self, min_, max_, min_scale=4, max_scale=20):
"""Compute an optimal scale between min and max"""
if min_ == 0 and max_ == 0:
return [0]
if max_ - min_ == 0:
return [min_]
if self.logarithmic:
log_scale = self._compute_logarithmic_scale(min_, max_)
if log_scale:
return log_scale
# else we fallback to normal scalling
order = round(log10(max(abs(min_), abs(max_)))) - 1
while (max_ - min_) / (10 ** order) < min_scale:
order -= 1
step = float(10 ** order)
while (max_ - min_) / step > max_scale:
step *= 2.
positions = []
position = round_to_scale(min_, step)
while position < (max_ + step):
rounded = round_to_scale(position, step)
if min_ <= rounded <= max_:
if rounded not in positions:
positions.append(rounded)
position += step
if len(positions) < 2:
return [min_, max_]
return positions
def _text_len(self, lenght, fs):
"""Approximation of text length"""
return lenght * 0.6 * fs
def _get_text_box(self, text, fs):
"""Approximation of text bounds"""
return (fs, self._text_len(len(text), fs))
def _get_texts_box(self, texts, fs):
"""Approximation of multiple texts bounds"""
max_len = max(map(len, texts))
return (fs, self._text_len(max_len, fs))
def _compute(self): def _compute(self):
"""Initial computations to draw the graph""" """Initial computations to draw the graph"""
@ -130,16 +78,16 @@ class BaseGraph(object):
def _compute_margin(self): def _compute_margin(self):
"""Compute graph margins from set texts""" """Compute graph margins from set texts"""
if self.show_legend: if self.show_legend:
h, w = self._get_texts_box( h, w = get_texts_box(
cut(self.series, 'title'), self.legend_font_size) cut(self.series, 'title'), self.legend_font_size)
self.margin.right += 10 + w + self.legend_box_size self.margin.right += 10 + w + self.legend_box_size
if self.title: if self.title:
h, w = self._get_text_box(self.title, self.title_font_size) h, w = get_text_box(self.title, self.title_font_size)
self.margin.top += 10 + h self.margin.top += 10 + h
if self._x_labels: if self._x_labels:
h, w = self._get_texts_box( h, w = get_texts_box(
cut(self._x_labels), self.label_font_size) cut(self._x_labels), self.label_font_size)
self.margin.bottom += 10 + max( self.margin.bottom += 10 + max(
w * sin(rad(self.x_label_rotation)), h) w * sin(rad(self.x_label_rotation)), h)
@ -148,7 +96,7 @@ class BaseGraph(object):
w * cos(rad(self.x_label_rotation)), w * cos(rad(self.x_label_rotation)),
self.margin.right) self.margin.right)
if self._y_labels: if self._y_labels:
h, w = self._get_texts_box( h, w = get_texts_box(
cut(self._y_labels), self.label_font_size) cut(self._y_labels), self.label_font_size)
self.margin.left += 10 + max( self.margin.left += 10 + max(
w * cos(rad(self.y_label_rotation)), h) w * cos(rad(self.y_label_rotation)), h)

74
pygal/graph/graph.py

@ -16,10 +16,17 @@
# #
# 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/>.
"""
Commmon graphing functions
"""
from __future__ import division from __future__ import division
from pygal.interpolate import interpolation
from pygal.graph.base import BaseGraph from pygal.graph.base import BaseGraph
from pygal.view import View, LogView from pygal.view import View, LogView
from pygal.util import is_major from pygal.util import is_major
from math import isnan, pi
class Graph(BaseGraph): class Graph(BaseGraph):
@ -43,49 +50,51 @@ class Graph(BaseGraph):
def _make_graph(self): def _make_graph(self):
"""Init common graph svg structure""" """Init common graph svg structure"""
self.graph_node = self.svg.node( self.nodes['graph'] = self.svg.node(
class_='graph %s-graph' % self.__class__.__name__.lower()) class_='graph %s-graph' % self.__class__.__name__.lower())
self.svg.node(self.graph_node, 'rect', self.svg.node(self.nodes['graph'], 'rect',
class_='background', class_='background',
x=0, y=0, x=0, y=0,
width=self.width, width=self.width,
height=self.height) height=self.height)
self.plot = self.svg.node( self.nodes['plot'] = self.svg.node(
self.graph_node, class_="plot", self.nodes['graph'], class_="plot",
transform="translate(%d, %d)" % ( transform="translate(%d, %d)" % (
self.margin.left, self.margin.top)) self.margin.left, self.margin.top))
self.svg.node(self.plot, 'rect', self.svg.node(self.nodes['plot'], 'rect',
class_='background', class_='background',
x=0, y=0, x=0, y=0,
width=self.view.width, width=self.view.width,
height=self.view.height) height=self.view.height)
self.overlay = self.svg.node( self.nodes['overlay'] = self.svg.node(
self.graph_node, class_="plot overlay", self.nodes['graph'], class_="plot overlay",
transform="translate(%d, %d)" % ( transform="translate(%d, %d)" % (
self.margin.left, self.margin.top)) self.margin.left, self.margin.top))
self.text_overlay = self.svg.node( self.nodes['text_overlay'] = self.svg.node(
self.graph_node, class_="plot text-overlay", self.nodes['graph'], class_="plot text-overlay",
transform="translate(%d, %d)" % ( transform="translate(%d, %d)" % (
self.margin.left, self.margin.top)) self.margin.left, self.margin.top))
tooltip_overlay = self.svg.node( self.nodes['tooltip_overlay'] = self.svg.node(
self.graph_node, class_="plot tooltip-overlay", self.nodes['graph'], class_="plot tooltip-overlay",
transform="translate(%d, %d)" % ( transform="translate(%d, %d)" % (
self.margin.left, self.margin.top)) self.margin.left, self.margin.top))
self.tooltip_node = self.svg.node(tooltip_overlay, id="tooltip", self.nodes['tooltip'] = self.svg.node(
self.nodes['tooltip_overlay'],
id="tooltip",
transform='translate(0 0)') transform='translate(0 0)')
self.svg.node(self.tooltip_node, 'rect', self.svg.node(self.nodes['tooltip'], 'rect',
id="tooltip-box", id="tooltip-box",
rx=5, ry=5, rx=5, ry=5,
) )
self.svg.node(self.tooltip_node, 'text') self.svg.node(self.nodes['tooltip'], 'text')
def _x_axis(self): def _x_axis(self):
"""Make the x axis: labels and guides""" """Make the x axis: labels and guides"""
if not self._x_labels: if not self._x_labels:
return return
axis = self.svg.node(self.plot, class_="axis x") axis = self.svg.node(self.nodes['plot'], class_="axis x")
if 0 not in [label[1] for label in self._x_labels]: if 0 not in [label[1] for label in self._x_labels]:
self.svg.node(axis, 'path', self.svg.node(axis, 'path',
@ -113,7 +122,7 @@ class Graph(BaseGraph):
if not self._y_labels: if not self._y_labels:
return return
axis = self.svg.node(self.plot, class_="axis y") axis = self.svg.node(self.nodes['plot'], class_="axis y")
if 0 not in [label[1] for label in self._y_labels]: if 0 not in [label[1] for label in self._y_labels]:
self.svg.node(axis, 'path', self.svg.node(axis, 'path',
@ -146,7 +155,7 @@ class Graph(BaseGraph):
if not self.show_legend: if not self.show_legend:
return return
legends = self.svg.node( legends = self.svg.node(
self.graph_node, class_='legends', self.nodes['graph'], class_='legends',
transform='translate(%d, %d)' % ( transform='translate(%d, %d)' % (
self.margin.left + self.view.width + 10, self.margin.left + self.view.width + 10,
self.margin.top + 10)) self.margin.top + 10))
@ -174,7 +183,7 @@ class Graph(BaseGraph):
def _title(self): def _title(self):
"""Make the title""" """Make the title"""
if self.title: if self.title:
self.svg.node(self.graph_node, 'text', class_='title', self.svg.node(self.nodes['graph'], 'text', class_='title',
x=self.margin.left + self.view.width / 2, x=self.margin.left + self.view.width / 2,
y=self.title_font_size + 10 y=self.title_font_size + 10
).text = self.title ).text = self.title
@ -183,11 +192,34 @@ class Graph(BaseGraph):
"""Make serie node""" """Make serie node"""
return dict( return dict(
plot=self.svg.node( plot=self.svg.node(
self.plot, self.nodes['plot'],
class_='series serie-%d color-%d' % (serie, serie)), class_='series serie-%d color-%d' % (serie, serie)),
overlay=self.svg.node( overlay=self.svg.node(
self.overlay, self.nodes['overlay'],
class_='series serie-%d color-%d' % (serie, serie)), class_='series serie-%d color-%d' % (serie, serie)),
text_overlay=self.svg.node( text_overlay=self.svg.node(
self.text_overlay, self.nodes['text_overlay'],
class_='series serie-%d color-%d' % (serie, serie))) class_='series serie-%d color-%d' % (serie, serie)))
def _interpolate(self, ys, xs,
polar=False, xy=False, xy_xmin=None, xy_rng=None):
"""Make the interpolation"""
interpolate = interpolation(
xs, ys, kind=self.interpolate)
p = self.interpolation_precision
xmin = min(xs)
xmax = max(xs)
interpolateds = []
for i in range(int(p + 1)):
x = i / p
if polar:
x = .5 * pi + 2 * pi * x
elif xy:
x = xy_xmin + xy_rng * x
interpolated = float(interpolate(x))
if not isnan(interpolated) and xmin <= x <= xmax:
coord = (x, interpolated)
if polar:
coord = tuple(reversed(coord))
interpolateds.append(coord)
return interpolateds

9
pygal/graph/horizontal.py

@ -16,6 +16,10 @@
# #
# 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/>.
"""
Horizontal graph
"""
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.graph.bar import Bar from pygal.graph.bar import Bar
from pygal.graph.stackedbar import StackedBar from pygal.graph.stackedbar import StackedBar
@ -25,13 +29,16 @@ class HorizontalGraph(Graph):
"""Horizontal graph""" """Horizontal graph"""
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.first_pass = True self.first_pass = True
kwargs['_horizontal'] = True self.horizontal = True
super(HorizontalGraph, self).__init__(*args, **kwargs) super(HorizontalGraph, self).__init__(*args, **kwargs)
def _compute(self): def _compute(self):
self.first_pass = False self.first_pass = False
# Stupid pylint
# pylint: disable-msg=E0203,W0201
if self.first_pass and self.x_labels: if self.first_pass and self.x_labels:
self.x_labels = list(reversed(self.x_labels)) self.x_labels = list(reversed(self.x_labels))
# pylint: enable-msg=W0201,E0203
super(HorizontalGraph, self)._compute() super(HorizontalGraph, self)._compute()
self._x_labels, self._y_labels = self._y_labels, self._x_labels self._x_labels, self._y_labels = self._y_labels, self._x_labels
self._box.swap() self._box.swap()

31
pygal/graph/line.py

@ -16,17 +16,20 @@
# #
# 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/>.
"""
Line chart
"""
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from pygal.util import cached_property from pygal.util import cached_property, compute_scale
from pygal.interpolate import interpolation
from math import isnan
class Line(Graph): class Line(Graph):
"""Line graph""" """Line graph"""
def _get_value(self, values, i): def _get_value(self, values, i):
"""Get the value formatted for tooltip"""
return self._format(values[i][1]) return self._format(values[i][1])
@cached_property @cached_property
@ -43,12 +46,14 @@ class Line(Graph):
if val[1] != None and (not self.logarithmic or val[1] > 0)] if val[1] != None and (not self.logarithmic or val[1] > 0)]
def _fill(self, values): def _fill(self, values):
"""Add extra values to fill the line"""
zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax)) zero = self.view.y(min(max(self.zero, self._box.ymin), self._box.ymax))
return ([(values[0][0], zero)] + return ([(values[0][0], zero)] +
values + values +
[(values[-1][0], zero)]) [(values[-1][0], zero)])
def line(self, serie_node, serie): def line(self, serie_node, serie):
"""Draw the line serie"""
view_values = map(self.view, serie.points) view_values = map(self.view, serie.points)
if self.show_dots: if self.show_dots:
for i, (x, y) in enumerate(view_values): for i, (x, y) in enumerate(view_values):
@ -88,21 +93,16 @@ class Line(Graph):
class_='line reactive' + (' nofill' if not self.fill else '')) class_='line reactive' + (' nofill' if not self.fill else ''))
def _compute(self): def _compute(self):
self._x_pos = [x / float(self._len - 1) for x in range(self._len) x_pos = [x / float(self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value ] if self._len != 1 else [.5] # Center if only one value
for serie in self.series: for serie in self.series:
if not hasattr(serie, 'points'): if not hasattr(serie, 'points'):
serie.points = [ serie.points = [
(self._x_pos[i], v) (x_pos[i], v)
for i, v in enumerate(serie.values)] for i, v in enumerate(serie.values)]
if self.interpolate: if self.interpolate:
interpolate = interpolation( serie.interpolated = self._interpolate(serie.values, x_pos)
self._x_pos, serie.values, kind=self.interpolate)
p = float(self.interpolation_precision)
serie.interpolated = [
(x / p, float(interpolate(x / p)))
for x in range(int(p + 1))
if not isnan(float(interpolate(x / p)))]
if self.include_x_axis: if self.include_x_axis:
self._box.ymin = min(min(self._values), 0) self._box.ymin = min(min(self._values), 0)
self._box.ymax = max(max(self._values), 0) self._box.ymax = max(max(self._values), 0)
@ -110,11 +110,12 @@ class Line(Graph):
self._box.ymin = min(self._values) self._box.ymin = min(self._values)
self._box.ymax = max(self._values) self._box.ymax = max(self._values)
self._y_pos = self._compute_scale(self._box.ymin, self._box.ymax y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else map(float, self.y_labels)
self._x_labels = self.x_labels and zip(self.x_labels, self._x_pos) self._x_labels = self.x_labels and zip(self.x_labels, x_pos)
self._y_labels = zip(map(self._format, self._y_pos), self._y_pos) self._y_labels = zip(map(self._format, y_pos), y_pos)
def _plot(self): def _plot(self):
for serie in self.series: for serie in self.series:

24
pygal/graph/pie.py

@ -16,6 +16,11 @@
# #
# 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/>.
"""
Pie chart
"""
from __future__ import division from __future__ import division
from pygal.graph.graph import Graph from pygal.graph.graph import Graph
from math import cos, sin, pi from math import cos, sin, pi
@ -31,6 +36,7 @@ class Pie(Graph):
def slice(self, serie_node, start_angle, angle, perc, def slice(self, serie_node, start_angle, angle, perc,
small=False): small=False):
"""Make a serie slice"""
val = '{0:.2%}'.format(perc) val = '{0:.2%}'.format(perc)
slices = self.svg.node(serie_node['plot'], class_="slices") slices = self.svg.node(serie_node['plot'], class_="slices")
slice_ = self.svg.node(slices, class_="slice") slice_ = self.svg.node(slices, class_="slice")
@ -51,16 +57,16 @@ class Pie(Graph):
else: else:
absolute_project = lambda rho, theta: fmt( absolute_project = lambda rho, theta: fmt(
diff(center, project(rho, theta))) diff(center, project(rho, theta)))
to1 = absolute_project(r, start_angle) to = [absolute_project(r, start_angle),
to2 = absolute_project(r, start_angle + angle) absolute_project(r, start_angle + angle),
to3 = absolute_project(small_r, start_angle + angle) absolute_project(small_r, start_angle + angle),
to4 = absolute_project(small_r, start_angle) absolute_project(small_r, start_angle)]
self.svg.node(slice_, 'path', self.svg.node(slice_, 'path',
d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % (
to1, to[0],
get_radius(r), int(angle > pi), to2, get_radius(r), int(angle > pi), to[1],
to3, to[2],
get_radius(small_r), int(angle > pi), to4), get_radius(small_r), int(angle > pi), to[3]),
class_='slice reactive tooltip-trigger') class_='slice reactive tooltip-trigger')
self.svg.node(slice_, 'desc', class_="value").text = val self.svg.node(slice_, 'desc', class_="value").text = val
tooltip_position = map( tooltip_position = map(
@ -97,7 +103,7 @@ class Pie(Graph):
angle, sum(serie.values) / total) angle, sum(serie.values) / total)
if len(serie.values) > 1: if len(serie.values) > 1:
small_current_angle = current_angle small_current_angle = current_angle
for i, val in enumerate(serie.values): for val in serie.values:
small_angle = 2 * pi * val / total small_angle = 2 * pi * val / total
self.slice( self.slice(
self._serie(serie.index), self._serie(serie.index),

58
pygal/graph/radar.py

@ -16,17 +16,26 @@
# #
# 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/>.
"""
Radar chart
"""
from __future__ import division from __future__ import division
from pygal.graph.line import Line from pygal.graph.line import Line
from pygal.view import PolarView from pygal.view import PolarView
from pygal.util import deg, cached_property from pygal.util import deg, cached_property, compute_scale
from pygal.interpolate import interpolation
from math import cos, pi from math import cos, pi
class Radar(Line): class Radar(Line):
"""Kiviat graph""" """Kiviat graph"""
def __init__(self, *args, **kwargs):
self.x_pos = None
self._rmax = None
super(Radar, self).__init__(*args, **kwargs)
def _fill(self, values): def _fill(self, values):
return values return values
@ -51,17 +60,17 @@ class Radar(Line):
if not self._x_labels: if not self._x_labels:
return return
axis = self.svg.node(self.plot, class_="axis x web") axis = self.svg.node(self.nodes['plot'], class_="axis x web")
format = lambda x: '%f %f' % x format_ = lambda x: '%f %f' % x
center = self.view((0, 0)) center = self.view((0, 0))
r = self._rmax r = self._rmax
for label, theta in self._x_labels: for label, theta in self._x_labels:
guides = self.svg.node(axis, class_='guides') guides = self.svg.node(axis, class_='guides')
end = self.view((r, theta)) end = self.view((r, theta))
self.svg.node(guides, 'path', self.svg.node(guides, 'path',
d='M%s L%s' % (format(center), format(end)), d='M%s L%s' % (format_(center), format_(end)),
class_='line') class_='line')
r_txt = (1 - self._box.__class__._margin) * self._box.ymax r_txt = (1 - self._box.__class__.margin) * self._box.ymax
pos_text = self.view((r_txt, theta)) pos_text = self.view((r_txt, theta))
text = self.svg.node(guides, 'text', text = self.svg.node(guides, 'text',
x=pos_text[0], x=pos_text[0],
@ -72,21 +81,21 @@ class Radar(Line):
if cos(angle) < 0: if cos(angle) < 0:
angle -= pi angle -= pi
text.attrib['transform'] = 'rotate(%f %s)' % ( text.attrib['transform'] = 'rotate(%f %s)' % (
deg(angle), format(pos_text)) deg(angle), format_(pos_text))
def _y_axis(self): def _y_axis(self):
if not self._y_labels: if not self._y_labels:
return return
axis = self.svg.node(self.plot, class_="axis y web") axis = self.svg.node(self.nodes['plot'], class_="axis y web")
for label, r in reversed(self._y_labels): for label, r in reversed(self._y_labels):
guides = self.svg.node(axis, class_='guides') guides = self.svg.node(axis, class_='guides')
self.svg.line( self.svg.line(
guides, [self.view((r, theta)) for theta in self._x_pos], guides, [self.view((r, theta)) for theta in self.x_pos],
close=True, close=True,
class_='guide line') class_='guide line')
x, y = self.view((r, self._x_pos[0])) x, y = self.view((r, self.x_pos[0]))
self.svg.node(guides, 'text', self.svg.node(guides, 'text',
x=x - 5, x=x - 5,
y=y y=y
@ -94,37 +103,34 @@ class Radar(Line):
def _compute(self): def _compute(self):
delta = 2 * pi / self._len delta = 2 * pi / self._len
self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] x_pos = [.5 * pi + i * delta for i in range(self._len + 1)]
for serie in self.series: for serie in self.series:
vals = list(serie.values) vals = list(serie.values)
vals.append(vals[0]) vals.append(vals[0])
vals = [val if val != None else 0 for val in vals] vals = [val if val != None else 0 for val in vals]
serie.points = [ serie.points = [
(v, self._x_pos[i]) (v, x_pos[i])
for i, v in enumerate(vals)] for i, v in enumerate(vals)]
if self.interpolate: if self.interpolate:
extend = 2 extend = 2
extended_x_pos = ( extended_x_pos = (
[.5 * pi + i * delta for i in range(-extend, 0)] + [.5 * pi + i * delta for i in range(-extend, 0)] +
self._x_pos + x_pos +
[.5 * pi + i * delta for i in range( [.5 * pi + i * delta for i in range(
self._len + 1, self._len + 1 + extend)]) self._len + 1, self._len + 1 + extend)])
extended_vals = vals[-extend:] + vals + vals[:extend] extended_vals = vals[-extend:] + vals + vals[:extend]
interpolate = interpolation( serie.interpolated = self._interpolate(
extended_x_pos, extended_vals, kind=self.interpolate) extended_vals, extended_x_pos, polar=True)
serie.interpolated = []
p = self.interpolation_precision self._box.margin *= 2
for s in range(int(p + 1)):
x = .5 * pi + 2 * pi * (s / p)
serie.interpolated.append((float(interpolate(x)), x))
self._box._margin *= 2
self._box.xmin = self._box.ymin = 0 self._box.xmin = self._box.ymin = 0
self._box.xmax = self._box.ymax = self._rmax = max(self._values) self._box.xmax = self._box.ymax = self._rmax = max(self._values)
self._y_pos = self._compute_scale( y_pos = compute_scale(
self._box.ymin, self._box.ymax, max_scale=8 self._box.ymin, self._box.ymax, self.logarithmic, max_scale=8
) if not self.y_labels else map(int, self.y_labels) ) if not self.y_labels else map(int, self.y_labels)
self._x_labels = self.x_labels and zip(self.x_labels, self._x_pos) self._x_labels = self.x_labels and zip(self.x_labels, x_pos)
self._y_labels = zip(map(self._format, self._y_pos), self._y_pos) self._y_labels = zip(map(self._format, y_pos), y_pos)
self._box.xmin = self._box.ymin = - self._box.ymax self._box.xmin = self._box.ymin = - self._box.ymax
self.x_pos = x_pos

18
pygal/graph/stackedbar.py

@ -16,8 +16,14 @@
# #
# 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/>.
"""
Stacked Bar chart
"""
from __future__ import division from __future__ import division
from pygal.graph.bar import Bar from pygal.graph.bar import Bar
from pygal.util import compute_scale
class StackedBar(Bar): class StackedBar(Bar):
@ -34,12 +40,12 @@ class StackedBar(Bar):
self._box.ymin, self._box.ymax = ( self._box.ymin, self._box.ymax = (
min(min(negative_vals), 0), max(max(positive_vals), 0)) min(min(negative_vals), 0), max(max(positive_vals), 0))
self._length = len(self.series[0].values)
x_pos = [x / self._length x_pos = [x / self._len
for x in range(self._length + 1) for x in range(self._len + 1)
] if self._length > 1 else [0, 1] # Center if only one value ] if self._len > 1 else [0, 1] # Center if only one value
y_pos = self._compute_scale(self._box.ymin, self._box.ymax y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic
) if not self.y_labels else map(float, self.y_labels) ) if not self.y_labels else map(float, self.y_labels)
self._x_ranges = zip(x_pos, x_pos[1:]) self._x_ranges = zip(x_pos, x_pos[1:])
@ -48,7 +54,7 @@ class StackedBar(Bar):
self._y_labels = zip(map(self._format, y_pos), y_pos) self._y_labels = zip(map(self._format, y_pos), y_pos)
def _plot(self): def _plot(self):
stack_vals = [[0, 0] for i in range(self._length)] stack_vals = [[0, 0] for i in range(self._len)]
for serie in self.series: for serie in self.series:
serie_node = self._serie(serie.index) serie_node = self._serie(serie.index)
stack_vals = self.bar( stack_vals = self.bar(

25
pygal/graph/stackedline.py

@ -16,17 +16,24 @@
# #
# 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/>.
"""
Stacked Line chart
"""
from pygal.graph.line import Line from pygal.graph.line import Line
from pygal.interpolate import interpolation
from math import isnan
from itertools import izip_longest from itertools import izip_longest
class StackedLine(Line): class StackedLine(Line):
"""Stacked Line graph""" """Stacked Line graph"""
def __init__(self, *args, **kwargs):
self._previous_line = None
super(StackedLine, self).__init__(*args, **kwargs)
def _fill(self, values): def _fill(self, values):
if not hasattr(self, '_previous_line'): if not self._previous_line:
self._previous_line = values self._previous_line = values
return super(StackedLine, self)._fill(values) return super(StackedLine, self)._fill(values)
new_values = values + list(reversed(self._previous_line)) new_values = values + list(reversed(self._previous_line))
@ -34,7 +41,7 @@ class StackedLine(Line):
return new_values return new_values
def _compute(self): def _compute(self):
self._x_pos = [x / float(self._len - 1) for x in range(self._len) x_pos = [x / float(self._len - 1) for x in range(self._len)
] if self._len != 1 else [.5] # Center if only one value ] if self._len != 1 else [.5] # Center if only one value
accumulation = [0] * self._len accumulation = [0] * self._len
for serie in self.series: for serie in self.series:
@ -43,14 +50,8 @@ class StackedLine(Line):
if val != None else 0 if val != None else 0
for val in serie.values], fillvalue=0)) for val in serie.values], fillvalue=0))
serie.points = [ serie.points = [
(self._x_pos[i], v) (x_pos[i], v)
for i, v in enumerate(accumulation)] for i, v in enumerate(accumulation)]
if self.interpolate: if self.interpolate:
interpolate = interpolation( serie.interpolated = self._interpolate(accumulation, x_pos)
self._x_pos, accumulation, kind=self.interpolate)
p = float(self.interpolation_precision)
serie.interpolated = [
(x / p, float(interpolate(x / p)))
for x in range(int(p + 1))
if not isnan(float(interpolate(x / p)))]
return super(StackedLine, self)._compute() return super(StackedLine, self)._compute()

34
pygal/graph/xy.py

@ -16,10 +16,14 @@
# #
# 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/>.
"""
XY Line graph
"""
from __future__ import division from __future__ import division
from pygal.util import compute_scale
from pygal.graph.line import Line from pygal.graph.line import Line
from pygal.interpolate import interpolation
from math import isnan
class XY(Line): class XY(Line):
@ -38,23 +42,17 @@ class XY(Line):
for val in serie.values for val in serie.values
if val[1] != None] if val[1] != None]
xmin = min(xvals) xmin = min(xvals)
xmax = max(xvals)
rng = (xmax - xmin)
for serie in self.series: for serie in self.series:
serie.points = serie.values serie.points = serie.values
if self.interpolate: if self.interpolate:
vals = zip(*serie.points) vals = zip(*sorted(serie.points, key=lambda x: x[0]))
interpolate = interpolation( serie.interpolated = self._interpolate(
vals[0], vals[1], kind=self.interpolate) vals[1], vals[0], xy=True, xy_xmin=xmin, xy_rng=rng)
serie_xmin = min(vals[0]) if not serie.interpolated:
serie_xmax = max(vals[0]) serie.interpolated = serie.values
serie.interpolated = []
r = (max(xvals) - xmin)
p = self.interpolation_precision
for s in range(int(p + 1)):
x = xmin + r * (s / p)
if (serie_xmin <= x <= serie_xmax and not
isnan(float(interpolate(x)))):
serie.interpolated.append((x, float(interpolate(x))))
if self.interpolate: if self.interpolate:
xvals = [val[0] xvals = [val[0]
@ -66,8 +64,10 @@ class XY(Line):
self._box.xmin, self._box.xmax = min(xvals), max(xvals) self._box.xmin, self._box.xmax = min(xvals), max(xvals)
self._box.ymin, self._box.ymax = min(yvals), max(yvals) self._box.ymin, self._box.ymax = min(yvals), max(yvals)
x_pos = self._compute_scale(self._box.xmin, self._box.xmax) x_pos = compute_scale(
y_pos = self._compute_scale(self._box.ymin, self._box.ymax) self._box.xmin, self._box.xmax, self.logarithmic)
y_pos = compute_scale(
self._box.ymin, self._box.ymax, self.logarithmic)
self._x_labels = zip(map(self._format, x_pos), x_pos) self._x_labels = zip(map(self._format, x_pos), x_pos)
self._y_labels = zip(map(self._format, y_pos), y_pos) self._y_labels = zip(map(self._format, y_pos), y_pos)

4
pygal/svg.py

@ -60,7 +60,7 @@ class Svg(object):
f.read(), f.read(),
style=self.graph.style, style=self.graph.style,
font_sizes=self.graph.font_sizes(), font_sizes=self.graph.font_sizes(),
hidden='y' if self.graph._horizontal else 'x') hidden='y' if self.graph.horizontal else 'x')
style.text = templ style.text = templ
def add_script(self, js): def add_script(self, js):
@ -92,7 +92,7 @@ class Svg(object):
def transposable_node(self, parent=None, tag='g', attrib=None, **extras): def transposable_node(self, parent=None, tag='g', attrib=None, **extras):
"""Make a new svg node which can be transposed if horizontal""" """Make a new svg node which can be transposed if horizontal"""
if self.graph._horizontal: if self.graph.horizontal:
for key1, key2 in (('x', 'y'), ('width', 'height')): for key1, key2 in (('x', 'y'), ('width', 'height')):
attr1 = extras.get(key1, None) attr1 = extras.get(key1, None)
attr2 = extras.get(key2, None) attr2 = extras.get(key2, None)

70
pygal/util.py

@ -23,7 +23,7 @@ Various utils
from __future__ import division from __future__ import division
from decimal import Decimal from decimal import Decimal
from math import floor, pi, log, log10 from math import floor, pi, log, log10, ceil
ORDERS = u"yzafpnµm kMGTPEZY" ORDERS = u"yzafpnµm kMGTPEZY"
@ -114,6 +114,74 @@ swap = lambda tuple_: tuple(reversed(tuple_))
ident = lambda x: x ident = lambda x: x
def compute_logarithmic_scale(min_, max_):
"""Compute an optimal scale for logarithmic"""
min_order = int(floor(log10(min_)))
max_order = int(ceil(log10(max_)))
positions = []
amplitude = max_order - min_order
if amplitude <= 1:
return []
detail = 10.
while amplitude * detail < 20:
detail *= 2
while amplitude * detail > 50:
detail /= 2
for order in range(min_order, max_order + 1):
for i in range(int(detail)):
tick = (10 * i / detail or 1) * 10 ** order
tick = round_to_scale(tick, tick)
if min_ <= tick <= max_ and tick not in positions:
positions.append(tick)
return positions
def compute_scale(min_, max_, logarithmic=False, min_scale=4, max_scale=20):
"""Compute an optimal scale between min and max"""
if min_ == 0 and max_ == 0:
return [0]
if max_ - min_ == 0:
return [min_]
if logarithmic:
log_scale = compute_logarithmic_scale(min_, max_)
if log_scale:
return log_scale
# else we fallback to normal scalling
order = round(log10(max(abs(min_), abs(max_)))) - 1
while (max_ - min_) / (10 ** order) < min_scale:
order -= 1
step = float(10 ** order)
while (max_ - min_) / step > max_scale:
step *= 2.
positions = []
position = round_to_scale(min_, step)
while position < (max_ + step):
rounded = round_to_scale(position, step)
if min_ <= rounded <= max_:
if rounded not in positions:
positions.append(rounded)
position += step
if len(positions) < 2:
return [min_, max_]
return positions
def text_len(lenght, fs):
"""Approximation of text length"""
return lenght * 0.6 * fs
def get_text_box(text, fs):
"""Approximation of text bounds"""
return (fs, text_len(len(text), fs))
def get_texts_box(texts, fs):
"""Approximation of multiple texts bounds"""
max_len = max(map(len, texts))
return (fs, text_len(max_len, fs))
# Stolen from brownie http://packages.python.org/Brownie/ # Stolen from brownie http://packages.python.org/Brownie/
class cached_property(object): class cached_property(object):
"""Optimize a static property""" """Optimize a static property"""

6
pygal/view.py

@ -45,7 +45,7 @@ class Margin(object):
class Box(object): class Box(object):
"""Chart boundings""" """Chart boundings"""
_margin = .02 margin = .02
def __init__(self): def __init__(self):
self.xmin = self.ymin = 0 self.xmin = self.ymin = 0
@ -73,11 +73,11 @@ class Box(object):
if not self.height: if not self.height:
self.ymin -= .5 self.ymin -= .5
self.ymax = self.ymin + 1 self.ymax = self.ymin + 1
xmargin = self._margin * self.width xmargin = self.margin * self.width
self.xmin -= xmargin self.xmin -= xmargin
self.xmax += xmargin self.xmax += xmargin
if with_margin: if with_margin:
ymargin = self._margin * self.height ymargin = self.margin * self.height
self.ymin -= ymargin self.ymin -= ymargin
self.ymax += ymargin self.ymax += ymargin

Loading…
Cancel
Save