diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 1ae3655..0b0b325 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -266,7 +266,6 @@ def get_test_routes(app): hist.add('2', [(2, 2, 8)]) return hist.render_response() - @app.route('/test/ylabels') def test_ylabels(): chart = Line() @@ -275,7 +274,6 @@ def get_test_routes(app): chart.add('line', [.0002, .0005, .00035]) return chart.render_response() - @app.route('/test/secondary/') def test_secondary_for(chart): chart = CHARTS_BY_NAME[chart](fill=True) @@ -463,4 +461,15 @@ def get_test_routes(app): return pie.render_response() - return filter(lambda x: x.startswith('test'), locals()) + @app.route('/test/legend_at_bottom/') + def test_legend_at_bottom_for(chart): + graph = CHARTS_BY_NAME[chart]() + graph.add('1', [1, 3, 12, 3, 4, None, 9]) + graph.add('2', [7, -4, 10, None, 8, 3, 1]) + graph.add('3', [7, -14, -10, None, 8, 3, 1]) + graph.add('4', [7, 4, -10, None, 8, 3, 1]) + graph.x_labels = ('a', 'b', 'c', 'd', 'e', 'f', 'g') + graph.legend_at_bottom = True + return graph.render_response() + + return list(filter(lambda x: x.startswith('test'), locals())) diff --git a/pygal/ghost.py b/pygal/ghost.py index d0ebd53..e9f5c86 100644 --- a/pygal/ghost.py +++ b/pygal/ghost.py @@ -88,9 +88,9 @@ class Ghost(object): def make_series(self, series): return prepare_values(series, self.config, self.cls) - def make_instance(self, overrides={}): + def make_instance(self, overrides=None): self.config(**self.__dict__) - self.config.__dict__.update(overrides) + self.config.__dict__.update(overrides or {}) series = self.make_series(self.raw_series) secondary_series = self.make_series(self.raw_series2) self._last__inst = self.cls( @@ -99,8 +99,10 @@ class Ghost(object): return self._last__inst # Rendering - def render(self, is_unicode=False): - return self.make_instance().render(is_unicode=is_unicode) + def render(self, is_unicode=False, **kwargs): + return (self + .make_instance(overrides=kwargs) + .render(is_unicode=is_unicode)) def render_tree(self): return self.make_instance().render_tree() @@ -171,6 +173,10 @@ class Ghost(object): spark_options.update(kwargs) return self.make_instance(spark_options).render() + def _repr_svg_(self): + """Display svg in IPython notebook""" + return self.render(disable_xml_declaration=True) + def _repr_png_(self): """Display png in IPython notebook""" return self.render_to_png() diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 26abb15..fe266bc 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -37,18 +37,17 @@ class Bar(Graph): super(Bar, self).__init__(*args, **kwargs) def _bar(self, parent, x, y, index, i, zero, - shift=True, secondary=False, rounded=False): + secondary=False, rounded=False): width = (self.view.x(1) - self.view.x(0)) / self._len x, y = self.view((x, y)) series_margin = width * self._series_margin x += series_margin width -= 2 * series_margin - if shift: - width /= self._order - x += index * width - serie_margin = width * self._serie_margin - x += serie_margin - width -= 2 * serie_margin + width /= self._order + x += index * width + serie_margin = width * self._serie_margin + x += serie_margin + width -= 2 * serie_margin height = self.view.y(zero) - y r = rounded * 1 if rounded else 0 self.svg.transposable_node( @@ -115,12 +114,10 @@ class Bar(Graph): min_0_ratio = (self.zero - self._box.ymin) / self._box.height or 1 max_0_ratio = (self._box.ymax - self.zero) / self._box.height or 1 - new_ymax = (self.zero - ymin) * (1 / min_0_ratio - 1) - new_ymin = -(ymax - self.zero) * (1 / max_0_ratio - 1) if ymax > self._box.ymax: - ymin = new_ymin + ymin = -(ymax - self.zero) * (1 / max_0_ratio - 1) else: - ymax = new_ymax + ymax = (self.zero - ymin) * (1 / min_0_ratio - 1) left_range = abs(self._box.ymax - self._box.ymin) right_range = abs(ymax - ymin) or 1 diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 3b100ec..52ec500 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -518,7 +518,7 @@ class Graph(BaseGraph): steps = len(y_pos) left_range = abs(y_pos[-1] - y_pos[0]) right_range = abs(ymax - ymin) or 1 - scale = right_range / (steps - 1) + scale = right_range / ((steps - 1) or 1) self._y_2nd_labels = [(self._format(ymin + i * scale), pos) for i, pos in enumerate(y_pos)] diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index 55ead91..27b8141 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -80,6 +80,8 @@ class StackedBar(Bar): if self.secondary_series: positive_vals, negative_vals = self._get_separated_values(True) + positive_vals = positive_vals or [self.zero] + negative_vals = negative_vals or [self.zero] self.secondary_negative_cumulation = [0] * self._len self.secondary_positive_cumulation = [0] * self._len self._pre_compute_secondary(positive_vals, negative_vals) @@ -91,7 +93,7 @@ class StackedBar(Bar): max(positive_vals), self.zero)) or self.zero def _bar(self, parent, x, y, index, i, zero, - shift=False, secondary=False, rounded=False): + secondary=False, rounded=False): if secondary: cumulation = (self.secondary_negative_cumulation if y < self.zero else diff --git a/pygal/graph/verticalpyramid.py b/pygal/graph/verticalpyramid.py index 074713a..242cd82 100644 --- a/pygal/graph/verticalpyramid.py +++ b/pygal/graph/verticalpyramid.py @@ -57,8 +57,8 @@ class VerticalPyramid(StackedBar): self._secondary_min = - self._secondary_max def _bar(self, parent, x, y, index, i, zero, - shift=True, secondary=False, rounded=False): + secondary=False, rounded=False): if index % 2: y = -y return super(VerticalPyramid, self)._bar( - parent, x, y, index, i, zero, False, secondary, rounded) + parent, x, y, index, i, zero, secondary, rounded) diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index df53d8e..48b0303 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -323,3 +323,34 @@ def test_inline_css(Chart): def test_meta_config(): from pygal.config import CONFIG_ITEMS assert all(c.name != 'Unbound' for c in CONFIG_ITEMS) + + +def test_label_rotation(Chart): + chart = Chart(x_label_rotation=28) + chart.add('1', [4, -5, 123, 59, 38]) + chart.add('2', [89, 0, 8, .12, 8]) + chart.x_labels = ['one', 'twoooooooooooooooooooooo', 'three', '4'] + q = chart.render_pyquery() + if Chart in (Line, Bar): + assert len(q('.guides text[transform^="rotate(28"]')) == 4 + + +def test_legend_at_bottom(Chart): + chart = Chart(legend_at_bottom=True) + chart.add('1', [4, -5, 123, 59, 38]) + chart.add('2', [89, 0, 8, .12, 8]) + chart.x_labels = ['one', 'twoooooooooooooooooooooo', 'three', '4'] + lab = chart.render() + chart.legend_at_bottom = False + assert lab != chart.render() + + +def test_x_y_title(Chart): + chart = Chart(title='I Am A Title', + x_title="I am a x title", + y_title="I am a y title") + chart.add('1', [4, -5, 123, 59, 38]) + chart.add('2', [89, 0, 8, .12, 8]) + chart.x_labels = ['one', 'twoooooooooooooooooooooo', 'three', '4'] + q = chart.render_pyquery() + assert len(q('.titles .title')) == 3 diff --git a/pygal/test/test_date.py b/pygal/test/test_date.py index 391a658..73016a4 100644 --- a/pygal/test/test_date.py +++ b/pygal/test/test_date.py @@ -56,3 +56,9 @@ def test_date(): '2013-02-01', '2013-03-01' ] + + +def test_date_overflow(): + datey = DateY(truncate_label=1000) + datey.add('dates', [1, 2, -1000000, 5, 100000000]) + assert datey.render_pyquery() diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 30c0a4a..b3b059b 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -21,12 +21,18 @@ import os import pygal import uuid import sys +import pytest from pygal import i18n from pygal.graph.frenchmap import DEPARTMENTS, REGIONS from pygal.util import cut from pygal._compat import u from pygal.test import pytest_generate_tests, make_data +try: + import cairosvg +except ImportError: + cairosvg = None + def test_multi_render(Chart, datas): chart = Chart() @@ -49,12 +55,8 @@ def test_render_to_file(Chart, datas): os.remove(file_name) +@pytest.mark.skipif(not cairosvg, reason="CairoSVG not installed") def test_render_to_png(Chart, datas): - try: - import cairosvg - except ImportError: - return - file_name = '/tmp/test_graph-%s.png' % uuid.uuid4() if os.path.exists(file_name): os.remove(file_name) @@ -62,8 +64,10 @@ def test_render_to_png(Chart, datas): chart = Chart() chart = make_data(chart, datas) chart.render_to_png(file_name) + png = chart._repr_png_() + with open(file_name, 'rb') as f: - assert f.read() + assert png == f.read() os.remove(file_name) @@ -353,3 +357,25 @@ def test_labels_with_links(Chart): assert len(links) == 4 # 3 links and 1 tooltip else: assert len(links) == 8 # 7 links and 1 tooltip + + +def test_sparkline(Chart, datas): + chart = Chart() + chart = make_data(chart, datas) + assert chart.render_sparkline() + + +def test_secondary(Chart): + chart = Chart() + rng = [83, .12, -34, 59] + chart.add('First serie', rng) + chart.add('Secondary serie', + map(lambda x: x * 2, rng), + secondary=True) + assert chart.render_pyquery() + + +def test_ipython_notebook(Chart, datas): + chart = Chart() + chart = make_data(chart, datas) + assert chart._repr_svg_() diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index b673f14..68723d3 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -100,3 +100,19 @@ def test_only_major_dots(): line.x_labels = map(str, range(12)) q = line.render_pyquery() assert len(q(".dots")) == 4 + + +def test_line_secondary(): + line = Line() + rng = [8, 12, 23, 73, 39, 57] + line.add('First serie', rng) + line.add('Secondary serie', + map(lambda x: x * 2, rng), + secondary=True) + line.title = "One serie" + q = line.render_pyquery() + assert len(q(".axis.x")) == 0 + assert len(q(".axis.y")) == 1 + assert len(q(".plot .series path")) == 2 + assert len(q(".x.axis .guides")) == 0 + assert len(q(".y.axis .guides")) == 7 diff --git a/pygal/test/test_view.py b/pygal/test/test_view.py new file mode 100644 index 0000000..93dff50 --- /dev/null +++ b/pygal/test/test_view.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# This file is part of pygal +# +# A python svg graph plotting library +# Copyright © 2012-2014 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 . + +from pygal.test import pytest_generate_tests, make_data + + +def test_all_logarithmic(Chart): + print(repr(Chart)) + chart = Chart(logarithmic=True) + chart.add('1', [1, 30, 8, 199, -23]) + chart.add('2', [87, 42, .9, 189, 81]) + assert chart.render() diff --git a/pygal/view.py b/pygal/view.py index 8a93c3a..45b96b0 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -255,8 +255,8 @@ class PolarThetaLogView(View): if not hasattr(box, '_tmin') or not hasattr(box, '_tmax'): raise Exception( 'Box must be set with set_polar_box for polar charts') - self.log10_tmax = log10(self.box._tmax) - self.log10_tmin = log10(self.box._tmin) + self.log10_tmax = log10(self.box._tmax) if self.box._tmax > 0 else 0 + self.log10_tmin = log10(self.box._tmin) if self.box._tmin > 0 else 0 def __call__(self, rhotheta): """Project rho and theta""" diff --git a/tox.ini b/tox.ini index 20cb90c..afaa5e9 100644 --- a/tox.ini +++ b/tox.ini @@ -7,6 +7,7 @@ deps = pytest pytest-cov pyquery + cairosvg commands = py.test [] --cov pygal pygal/test