diff --git a/demo/moulinrouge.py b/demo/moulinrouge.py index 6879b1b..7abb14b 100755 --- a/demo/moulinrouge.py +++ b/demo/moulinrouge.py @@ -51,10 +51,11 @@ else: app.logger.debug('HTTPServer monkey patched for url %s' % url) try: - import wdb + from wdb.ext import WdbMiddleware, add_w_builtin except ImportError: pass else: - app.wsgi_app = wdb.Wdb(app.wsgi_app, start_disabled=True) + add_w_builtin() + app.wsgi_app = WdbMiddleware(app.wsgi_app, start_disabled=True) app.run(debug=True, threaded=True, host='0.0.0.0', port=21112) diff --git a/pygal/config.py b/pygal/config.py index 14b1f07..ba06a9a 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -180,7 +180,7 @@ class Config(object): False, bool, "Value", "Display values in logarithmic scale") interpolate = Key( - None, str, "Value", "Interpolation, this requires scipy module", + None, str, "Value", "Interpolation. ", "May be any of 'linear', 'nearest', 'zero', 'slinear', 'quadratic," "'cubic', 'krogh', 'barycentric', 'univariate'," "or an integer specifying the order" diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 689a65f..31f8b4d 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -216,7 +216,7 @@ class BaseGraph(object): @cached_property def _order(self): - """Getter for the maximum series value""" + """Getter for the number of series""" return len(self.all_series) def _draw(self): diff --git a/pygal/graph/datey.py b/pygal/graph/datey.py index b72c8b7..54a666c 100644 --- a/pygal/graph/datey.py +++ b/pygal/graph/datey.py @@ -95,8 +95,7 @@ class DateY(XY): vals = list(zip(*sorted( [t for t in serie.points if None not in t], key=lambda x: x[0]))) - serie.interpolated = self._interpolate( - vals[1], vals[0], xy=True, xy_xmin=xmin, xy_rng=rng) + serie.interpolated = self._interpolate(vals[0], vals[1]) if self.interpolate and rng: xvals = [val[0] diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 50d87a4..26f4b97 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -22,7 +22,7 @@ Commmon graphing functions """ from __future__ import division -from pygal.interpolate import interpolation +from pygal.interpolate import cubic_interpolate from pygal.graph.base import BaseGraph from pygal.view import View, LogView, XYLogView from pygal.util import ( @@ -375,29 +375,16 @@ class Graph(BaseGraph): self.nodes['text_overlay'], class_='series serie-%d color-%d' % (serie, serie % 16))) - def _interpolate( - self, ys, xs, - polar=False, xy=False, xy_xmin=None, xy_rng=None): + def _interpolate(self, xs, ys): """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 + x = [] + y = [] + for i in range(len(ys)): + if ys[i] is not None: + x.append(xs[i]) + y.append(ys[i]) + + return list(cubic_interpolate(x, y, self.interpolation_precision)) def _tooltip_data(self, node, value, x, y, classes=None): self.svg.node(node, 'desc', class_="value").text = value @@ -433,7 +420,7 @@ class Graph(BaseGraph): (x_pos[i], v) for i, v in enumerate(serie.values)] if serie.points and self.interpolate: - serie.interpolated = self._interpolate(serie.values, x_pos) + serie.interpolated = self._interpolate(x_pos, serie.values) else: serie.interpolated = [] diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index 68ed20d..55bfa0f 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -143,17 +143,15 @@ class Radar(Line): (v, x_pos[i]) for i, v in enumerate(serie.values)] if self.interpolate: - extend = 2 extended_x_pos = ( - [.5 * pi + i * delta for i in range(-extend, 0)] + - x_pos + - [.5 * pi + i * delta for i in range( - self._len + 1, self._len + 1 + extend)]) - extended_vals = (serie.values[-extend:] + - serie.values + - serie.values[:extend]) - serie.interpolated = self._interpolate( - extended_vals, extended_x_pos, polar=True) + [.5 * pi - delta] + x_pos) + extended_vals = (serie.values[-1:] + + serie.values) + serie.interpolated = list( + map(tuple, + map(reversed, + self._interpolate( + extended_x_pos, extended_vals)))) # x labels space self._box.margin *= 2 diff --git a/pygal/graph/stackedline.py b/pygal/graph/stackedline.py index 1965cfd..ca2e0dc 100644 --- a/pygal/graph/stackedline.py +++ b/pygal/graph/stackedline.py @@ -51,6 +51,6 @@ class StackedLine(Line): (x_pos[i], v) for i, v in enumerate(accumulation)] if serie.points and self.interpolate: - serie.interpolated = self._interpolate(accumulation, x_pos) + serie.interpolated = self._interpolate(x_pos, accumulation) else: serie.interpolated = [] diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py index 213c234..cfae4ba 100644 --- a/pygal/graph/xy.py +++ b/pygal/graph/xy.py @@ -75,8 +75,7 @@ class XY(Line): vals = list(zip(*sorted( filter(lambda t: None not in t, serie.points), key=lambda x: x[0]))) - serie.interpolated = self._interpolate( - vals[1], vals[0], xy=True, xy_xmin=xmin, xy_rng=xrng) + serie.interpolated = self._interpolate(vals[0], vals[1]) if self.interpolate and xrng: self.xvals = [val[0] diff --git a/pygal/interpolate.py b/pygal/interpolate.py index be56667..926973f 100644 --- a/pygal/interpolate.py +++ b/pygal/interpolate.py @@ -17,45 +17,46 @@ # You should have received a copy of the GNU Lesser General Public License # along with pygal. If not, see . """ -Interpolation using scipy +Interpolation """ -from pygal.util import ident +from __future__ import division -scipy = None -try: - import scipy - from scipy import interpolate -except ImportError: - pass -KINDS = ['cubic', 'univariate', 'quadratic', 'slinear', 'nearest', 'zero'] +def cubic_interpolate(x, a, precision=250): + """Takes a list of (x, a) and returns an iterator over + the natural cubic spline of points with `precision` points between them""" + n = len(x) - 1 + # Spline equation is a + bx + cx² + dx³ + # ie: Spline part i equation is a[i] + b[i]x + c[i]x² + d[i]x³ + b = [0] * (n + 1) + c = [0] * (n + 1) + d = [0] * (n + 1) + m = [0] * (n + 1) + z = [0] * (n + 1) + h = [x2 - x1 for x1, x2 in zip(x, x[1:])] + k = [a2 - a1 for a1, a2 in zip(a, a[1:])] + g = [k[i] / h[i] if h[i] else 1 for i in range(n)] -def interpolation(x, y, kind): - """Make the interpolation function""" - assert scipy is not None, ( - 'You must have scipy installed to use interpolation') - order = None - if len(y) < len(x): - x = x[:len(y)] + for i in range(1, n): + j = i - 1 + l = 1 / (2 * (x[i + 1] - x[j]) - h[j] * m[j]) + m[i] = h[i] * l + z[i] = (3 * (g[i] - g[j]) - h[j] * z[j]) * l - pack = list(zip(*filter(lambda t: None not in t, zip(x, y)))) - if len(pack) == 0: - return ident - x, y = pack - if len(x) < 2: - return ident - if isinstance(kind, int): - order = kind - elif kind in KINDS: - order = {'nearest': 0, 'zero': 0, 'slinear': 1, - 'quadratic': 2, 'cubic': 3, 'univariate': 3}[kind] - if order and len(x) <= order: - kind = len(x) - 1 - if kind == 'krogh': - return interpolate.KroghInterpolator(x, y) - elif kind == 'barycentric': - return interpolate.BarycentricInterpolator(x, y) - elif kind == 'univariate': - return interpolate.InterpolatedUnivariateSpline(x, y) - return interpolate.interp1d(x, y, kind=kind, bounds_error=False) + for j in reversed(range(n)): + if h[j] == 0: + continue + c[j] = z[j] - (m[j] * c[j + 1]) + b[j] = g[j] - (h[j] * (c[j + 1] + 2 * c[j])) / 3 + d[j] = (c[j + 1] - c[j]) / (3 * h[j]) + + for i in range(n + 1): + yield x[i], a[i] + if i == n or h[i] == 0: + continue + for s in range(1, precision): + X = s * h[i] / precision + X2 = X * X + X3 = X2 * X + yield x[i] + X, a[i] + b[i] * X + c[i] * X2 + d[i] * X3 diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index 9e49933..3d42f81 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -31,6 +31,16 @@ def test_config_behaviours(): line1.add('_', [1, 2, 3]) l1 = line1.render() + q = line1.render_pyquery() + assert len(q(".axis.x")) == 1 + assert len(q(".axis.y")) == 1 + assert len(q(".plot .series path")) == 1 + assert len(q(".legend")) == 0 + assert len(q(".x.axis .guides")) == 3 + assert len(q(".y.axis .guides")) == 21 + assert len(q(".dots")) == 3 + assert q(".axis.x text").map(texts) == ['a', 'b', 'c'] + line2 = Line( show_legend=False, fill=True, @@ -56,6 +66,17 @@ def test_config_behaviours(): l4 = line4.render() assert l1 == l4 + line_config = Config() + line_config.show_legend = False + line_config.fill = True + line_config.pretty_print = True + line_config.x_labels = ['a', 'b', 'c'] + + line5 = Line(line_config) + line5.add('_', [1, 2, 3]) + l5 = line5.render() + assert l1 == l5 + def test_config_alterations_class(): class LineConfig(Config):