From e76c398b0cd2f8531d9573281424157d4b875c47 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Thu, 4 Oct 2012 11:58:51 +0200 Subject: [PATCH] New no data --- demo/moulinrouge/tests.py | 22 +++++++++++++------- pygal/config.py | 2 ++ pygal/css/base.css | 5 +++++ pygal/graph/bar.py | 1 + pygal/graph/base.py | 30 +++++++++++++++----------- pygal/graph/gauge.py | 19 +++++++++-------- pygal/graph/radar.py | 7 ++++--- pygal/graph/stackedbar.py | 5 ++--- pygal/graph/xy.py | 18 +++++++++------- pygal/svg.py | 6 +++--- pygal/test/test_config.py | 4 ++-- pygal/test/test_graph.py | 16 +++++++------- pygal/test/test_line.py | 6 ++---- pygal/test/test_util.py | 5 +++-- pygal/util.py | 2 -- pygal/view.py | 44 +++++++++++++++++++++++++++++++++++---- 16 files changed, 126 insertions(+), 66 deletions(-) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index f434e7e..9453a8d 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -56,12 +56,6 @@ def get_test_routes(app): bar.title = '123456789 ' * 30 return bar.render_response() - @app.route('/test/no_data') - def test_no_data(): - bar = Bar() - bar.title = '123456789 ' * 30 - return bar.render_response() - @app.route('/test/none') def test_bar_none(): bar = Bar() @@ -126,10 +120,24 @@ def get_test_routes(app): @app.route('/test/one/') def test_one_for(chart): graph = CHARTS_BY_NAME[chart]() - graph.add('1', [1, 2]) + graph.add('1', [10]) graph.x_labels = 'a', return graph.render_response() + @app.route('/test/no_data/') + def test_no_data_for(chart): + graph = CHARTS_BY_NAME[chart]() + graph.add('Empty 1', []) + graph.add('Empty 2', []) + graph.x_labels = 'empty' + graph.title = '123456789 ' * 30 + return graph.render_response() + + @app.route('/test/no_data/at_all/') + def test_no_data_at_all_for(chart): + graph = CHARTS_BY_NAME[chart]() + return graph.render_response() + @app.route('/test/interpolate/') def test_interpolate_for(chart): graph = CHARTS_BY_NAME[chart](interpolate='cubic') diff --git a/pygal/config.py b/pygal/config.py index eca7f92..17e127c 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -196,6 +196,8 @@ class Config(object): legend_font_size = Key(14, int, "Text", "Legend font size") + no_data_font_size = Key(64, int, "Text", "No data text font size") + print_values = Key( True, bool, "Text", "Print values when graph is in non interactive mode") diff --git a/pygal/css/base.css b/pygal/css/base.css index 5391838..8d5a65b 100644 --- a/pygal/css/base.css +++ b/pygal/css/base.css @@ -46,3 +46,8 @@ font-family: monospace; font-size: {{ font_sizes.tooltip }}; } + +text.no_data { + font-size: {{ font_sizes.no_data }}; +} + diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index c97ce79..abbef8b 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -80,6 +80,7 @@ class Bar(Graph): def _compute(self): self._box.ymin = min(self._min, self.zero) self._box.ymax = max(self._max, self.zero) + x_pos = [ x / self._len for x in range(self._len + 1) ] if self._len > 1 else [0, 1] # Center if only one value diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 135b4b3..5b413d2 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -38,7 +38,7 @@ class BaseGraph(object): def __init__(self, config, series): """Init the graph""" self.config = config - self.series = series + self.series = series or [] self.horizontal = getattr(self, 'horizontal', False) self.svg = Svg(self) self._x_labels = None @@ -52,11 +52,7 @@ class BaseGraph(object): self.zero = min(filter( lambda x: x > 0, [val for serie in self.series for val in serie.safe_values])) - if self.series and self._has_data(): - self._draw() - else: - self.svg.draw_no_data() - + self._draw() self.svg.pre_render() def __getattr__(self, attr): @@ -66,6 +62,9 @@ class BaseGraph(object): return object.__getattribute__(self, attr) def _split_title(self): + if not self.title: + self.title = [] + return size = reverse_text_len(self.width, self.title_font_size) title = self.title.strip() self.title = [] @@ -91,7 +90,7 @@ class BaseGraph(object): def _compute_margin(self): """Compute graph margins from set texts""" - if self.show_legend: + if self.show_legend and self.series: h, w = get_texts_box( map(lambda x: truncate(x, self.truncate_legend or 15), cut(self.series, 'title')), @@ -141,17 +140,19 @@ class BaseGraph(object): @cached_property def _len(self): """Getter for the maximum series size""" - return max([len(serie.values) for serie in self.series]) + return max([len(serie.values) for serie in self.series] or [0]) @cached_property def _min(self): """Getter for the minimum series value""" - return (self.range and self.range[0]) or min(self._values) + return (self.range and self.range[0]) or ( + min(self._values) if self._values else None) @cached_property def _max(self): """Getter for the maximum series value""" - return (self.range and self.range[1]) or max(self._values) + return (self.range and self.range[1]) or ( + max(self._values) if self._values else None) @cached_property def _order(self): @@ -164,11 +165,16 @@ class BaseGraph(object): self._split_title() self._compute_margin() self._decorate() - self._plot() + if self.series and self._has_data(): + self._plot() + else: + self.svg.draw_no_data() def _has_data(self): """Check if there is any data""" - return sum(map(len, map(lambda s: s.safe_values, self.series))) != 0 + return sum( + map(len, map(lambda s: s.safe_values, self.series))) != 0 and ( + sum(map(abs, self._values)) != 0) def render(self, is_unicode): """Render the graph, and return the svg string""" diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py index b8c7e02..84c92d4 100644 --- a/pygal/graph/gauge.py +++ b/pygal/graph/gauge.py @@ -39,15 +39,15 @@ class Gauge(Graph): def arc_pos(self, value): aperture = pi / 3 - if value > self._max: + if value > self.max_: return (3 * pi - aperture / 2) / 2 - if value < self._min: + if value < self.min_: return (3 * pi + aperture / 2) / 2 start = 3 * pi / 2 + aperture / 2 return start + (2 * pi - aperture) * ( value - self.min_) / (self.max_ - self.min_) - def needle(self, serie_node, serie,): + def needle(self, serie_node, serie): thickness = .05 for i, value in enumerate(serie.values): if value is None: @@ -95,10 +95,11 @@ class Gauge(Graph): else '')) x, y = self.view((.9, theta)) - self.svg.node(guides, 'text', - x=x, - y=y - ).text = label + self.svg.node( + guides, 'text', + x=x, + y=y + ).text = label def _y_axis(self, draw_axes=True): axis = self.svg.node(self.nodes['plot'], class_="axis y gauge") @@ -109,8 +110,8 @@ class Gauge(Graph): self._box.xmin = -1 self._box.ymin = -1 - self.min_ = self._min - self.max_ = self._max + self.min_ = self._min or 0 + self.max_ = self._max or 0 if self.max_ - self.min_ == 0: self.min_ -= 1 self.max_ += 1 diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index fdf3fb3..29142d1 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -106,7 +106,7 @@ class Radar(Line): y=y).text = label def _compute(self): - delta = 2 * pi / self._len + delta = 2 * pi / self._len if self._len else 0 x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] for serie in self.series: serie.points = [ @@ -126,8 +126,9 @@ class Radar(Line): extended_vals, extended_x_pos, polar=True) self._box.margin *= 2 - self._box.xmin = self._box.ymin = - self._max - self._box.xmax = self._box.ymax = self._rmax = self._max + _max = self._max or 1 + self._box.xmin = self._box.ymin = - _max + self._box.xmax = self._box.ymax = self._rmax = _max y_pos = compute_scale( 0, self._box.ymax, self.logarithmic, self.order_min, max_scale=8 diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index ec5337e..fe98148 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -46,9 +46,8 @@ class StackedBar(Bar): return positive_vals, negative_vals def _compute_box(self, positive_vals, negative_vals): - self._box.ymin, self._box.ymax = ( - min(min(negative_vals), self.zero), - max(max(positive_vals), self.zero)) + self._box.ymin = negative_vals and min(min(negative_vals), self.zero) + self._box.ymax = positive_vals and max(max(positive_vals), self.zero) def _compute(self): positive_vals, negative_vals = self._get_separated_values() diff --git a/pygal/graph/xy.py b/pygal/graph/xy.py index 8e320fa..da7eecb 100644 --- a/pygal/graph/xy.py +++ b/pygal/graph/xy.py @@ -41,29 +41,33 @@ class XY(Line): for serie in self.series for val in serie.values if val[1] is not None] - xmin = min(xvals) - xmax = max(xvals) - rng = (xmax - xmin) + if xvals: + xmin = min(xvals) + xmax = max(xvals) + rng = (xmax - xmin) + else: + rng = None for serie in self.series: serie.points = serie.values - if self.interpolate: + if self.interpolate and rng: vals = 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=rng) - if self.interpolate: + if self.interpolate and rng: xvals = [val[0] for serie in self.series for val in serie.interpolated] yvals = [val[1] for serie in self.series for val in serie.interpolated] + if rng: + self._box.xmin, self._box.xmax = min(xvals), max(xvals) + self._box.ymin, self._box.ymax = min(yvals), max(yvals) - self._box.xmin, self._box.xmax = min(xvals), max(xvals) - self._box.ymin, self._box.ymax = min(yvals), max(yvals) x_pos = compute_scale( self._box.xmin, self._box.xmax, self.logarithmic, self.order_min) y_pos = compute_scale( diff --git a/pygal/svg.py b/pygal/svg.py index bdb31fc..3f1a2f6 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -194,9 +194,9 @@ class Svg(object): self.root.set('height', str(self.graph.height)) def draw_no_data(self): - no_data = self.node(self.root, 'text', - x=self.graph.width / 2, - y=self.graph.height / 2, + no_data = self.node(self.graph.nodes['text_overlay'], 'text', + x=self.graph.view.width / 2, + y=self.graph.view.height / 2, class_='no_data') no_data.text = self.graph.no_data_text diff --git a/pygal/test/test_config.py b/pygal/test/test_config.py index 0b5ecd7..52f3502 100644 --- a/pygal/test/test_config.py +++ b/pygal/test/test_config.py @@ -212,7 +212,7 @@ def test_show_dots(): def test_no_data(): line = Line() q = line.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" line.no_data_text = u"þæ®þ怀&ij¿’€" q = line.render_pyquery() - assert q("text").text() == u"þæ®þ怀&ij¿’€" + assert q(".text-overlay text").text() == u"þæ®þ怀&ij¿’€" diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index 57901fc..e0de73c 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -95,7 +95,7 @@ def test_empty_lists(Chart): chart.add('B', []) chart.x_labels = ('red', 'green', 'blue') q = chart.render_pyquery() - assert len(q(".legend")) == 1 + assert len(q(".legend")) == 2 def test_empty_lists_with_nones(Chart): @@ -104,7 +104,7 @@ def test_empty_lists_with_nones(Chart): chart.add('B', [None, 4, 4]) chart.x_labels = ('red', 'green', 'blue') q = chart.render_pyquery() - assert len(q(".legend")) == 1 + assert len(q(".legend")) == 2 def test_non_iterable_value(Chart): @@ -160,14 +160,14 @@ def test_values_by_dict(Chart): def test_no_data_with_no_values(Chart): chart = Chart() q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" def test_no_data_with_empty_serie(Chart): chart = Chart() chart.add('Serie', []) q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" def test_no_data_with_empty_series(Chart): @@ -175,21 +175,21 @@ def test_no_data_with_empty_series(Chart): chart.add('Serie1', []) chart.add('Serie2', []) q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" def test_no_data_with_none(Chart): chart = Chart() chart.add('Serie', None) q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" def test_no_data_with_list_of_none(Chart): chart = Chart() chart.add('Serie', [None]) q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" def test_no_data_with_lists_of_nones(Chart): @@ -197,4 +197,4 @@ def test_no_data_with_lists_of_nones(Chart): chart.add('Serie1', [None, None, None, None]) chart.add('Serie2', [None, None, None]) q = chart.render_pyquery() - assert q("text").text() == "No data" + assert q(".text-overlay text").text() == "No data" diff --git a/pygal/test/test_line.py b/pygal/test/test_line.py index 5dd28a4..5cfe448 100644 --- a/pygal/test/test_line.py +++ b/pygal/test/test_line.py @@ -75,11 +75,9 @@ def test_no_dot(): line = Line() line.add('no dot', []) q = line.render_pyquery() - assert q(".plot") == [] - assert q("text").text() == 'No data' + assert q(".text-overlay text").text() == 'No data' def test_no_dot_at_all(): q = Line().render_pyquery() - assert q(".plot") == [] - assert q("text").text() == 'No data' + assert q(".text-overlay text").text() == 'No data' diff --git a/pygal/test/test_util.py b/pygal/test/test_util.py index 441365d..85fb0bb 100644 --- a/pygal/test/test_util.py +++ b/pygal/test/test_util.py @@ -73,8 +73,9 @@ def test_format(): obj.a = 1 obj.b = True obj.c = '3' - assert template('foo {{ o.a }} {{o.b}}-{{o.c}}', - o=obj) == 'foo 1 True-3' + assert template( + 'foo {{ o.a }} {{o.b}}-{{o.c}}', + o=obj) == 'foo 1 True-3' def test_humanize(): diff --git a/pygal/util.py b/pygal/util.py index d35c363..17ce8d9 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -318,8 +318,6 @@ def prepare_values(raw, config, cls): else: raw_values = list(raw_values) - if not filter(lambda x: x is not None, raw_values): - continue for index, raw_value in enumerate( raw_values + ( (width - len(raw_values)) * [None] # aligning values diff --git a/pygal/view.py b/pygal/view.py index a7a4b5b..22fd636 100644 --- a/pygal/view.py +++ b/pygal/view.py @@ -48,10 +48,46 @@ class Box(object): margin = .02 def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): - self.xmin = xmin - self.ymin = ymin - self.xmax = xmax - self.ymax = ymax + self._xmin = xmin + self._ymin = ymin + self._xmax = xmax + self._ymax = ymax + + @property + def xmin(self): + return self._xmin + + @xmin.setter + def xmin(self, value): + if value: + self._xmin = value + + @property + def ymin(self): + return self._ymin + + @ymin.setter + def ymin(self, value): + if value: + self._ymin = value + + @property + def xmax(self): + return self._xmax + + @xmax.setter + def xmax(self, value): + if value: + self._xmax = value + + @property + def ymax(self): + return self._ymax + + @ymax.setter + def ymax(self, value): + if value: + self._ymax = value @property def width(self):