Browse Source

New no data

pull/8/head
Florian Mounier 12 years ago
parent
commit
e76c398b0c
  1. 22
      demo/moulinrouge/tests.py
  2. 2
      pygal/config.py
  3. 5
      pygal/css/base.css
  4. 1
      pygal/graph/bar.py
  5. 26
      pygal/graph/base.py
  6. 13
      pygal/graph/gauge.py
  7. 7
      pygal/graph/radar.py
  8. 5
      pygal/graph/stackedbar.py
  9. 10
      pygal/graph/xy.py
  10. 6
      pygal/svg.py
  11. 4
      pygal/test/test_config.py
  12. 16
      pygal/test/test_graph.py
  13. 6
      pygal/test/test_line.py
  14. 3
      pygal/test/test_util.py
  15. 2
      pygal/util.py
  16. 44
      pygal/view.py

22
demo/moulinrouge/tests.py

@ -56,12 +56,6 @@ def get_test_routes(app):
bar.title = '123456789 ' * 30 bar.title = '123456789 ' * 30
return bar.render_response() 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') @app.route('/test/none')
def test_bar_none(): def test_bar_none():
bar = Bar() bar = Bar()
@ -126,10 +120,24 @@ def get_test_routes(app):
@app.route('/test/one/<chart>') @app.route('/test/one/<chart>')
def test_one_for(chart): def test_one_for(chart):
graph = CHARTS_BY_NAME[chart]() graph = CHARTS_BY_NAME[chart]()
graph.add('1', [1, 2]) graph.add('1', [10])
graph.x_labels = 'a', graph.x_labels = 'a',
return graph.render_response() return graph.render_response()
@app.route('/test/no_data/<chart>')
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/<chart>')
def test_no_data_at_all_for(chart):
graph = CHARTS_BY_NAME[chart]()
return graph.render_response()
@app.route('/test/interpolate/<chart>') @app.route('/test/interpolate/<chart>')
def test_interpolate_for(chart): def test_interpolate_for(chart):
graph = CHARTS_BY_NAME[chart](interpolate='cubic') graph = CHARTS_BY_NAME[chart](interpolate='cubic')

2
pygal/config.py

@ -196,6 +196,8 @@ class Config(object):
legend_font_size = Key(14, int, "Text", "Legend font size") 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( print_values = Key(
True, bool, True, bool,
"Text", "Print values when graph is in non interactive mode") "Text", "Print values when graph is in non interactive mode")

5
pygal/css/base.css

@ -46,3 +46,8 @@
font-family: monospace; font-family: monospace;
font-size: {{ font_sizes.tooltip }}; font-size: {{ font_sizes.tooltip }};
} }
text.no_data {
font-size: {{ font_sizes.no_data }};
}

1
pygal/graph/bar.py

@ -80,6 +80,7 @@ class Bar(Graph):
def _compute(self): def _compute(self):
self._box.ymin = min(self._min, self.zero) self._box.ymin = min(self._min, self.zero)
self._box.ymax = max(self._max, self.zero) self._box.ymax = max(self._max, self.zero)
x_pos = [ x_pos = [
x / self._len for x in range(self._len + 1) x / self._len for x in range(self._len + 1)
] if self._len > 1 else [0, 1] # Center if only one value ] if self._len > 1 else [0, 1] # Center if only one value

26
pygal/graph/base.py

@ -38,7 +38,7 @@ class BaseGraph(object):
def __init__(self, config, series): def __init__(self, config, series):
"""Init the graph""" """Init the graph"""
self.config = config self.config = config
self.series = series self.series = series or []
self.horizontal = getattr(self, 'horizontal', False) self.horizontal = getattr(self, 'horizontal', False)
self.svg = Svg(self) self.svg = Svg(self)
self._x_labels = None self._x_labels = None
@ -52,11 +52,7 @@ class BaseGraph(object):
self.zero = min(filter( self.zero = min(filter(
lambda x: x > 0, lambda x: x > 0,
[val for serie in self.series for val in serie.safe_values])) [val for serie in self.series for val in serie.safe_values]))
if self.series and self._has_data():
self._draw() self._draw()
else:
self.svg.draw_no_data()
self.svg.pre_render() self.svg.pre_render()
def __getattr__(self, attr): def __getattr__(self, attr):
@ -66,6 +62,9 @@ class BaseGraph(object):
return object.__getattribute__(self, attr) return object.__getattribute__(self, attr)
def _split_title(self): def _split_title(self):
if not self.title:
self.title = []
return
size = reverse_text_len(self.width, self.title_font_size) size = reverse_text_len(self.width, self.title_font_size)
title = self.title.strip() title = self.title.strip()
self.title = [] self.title = []
@ -91,7 +90,7 @@ 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 and self.series:
h, w = get_texts_box( h, w = get_texts_box(
map(lambda x: truncate(x, self.truncate_legend or 15), map(lambda x: truncate(x, self.truncate_legend or 15),
cut(self.series, 'title')), cut(self.series, 'title')),
@ -141,17 +140,19 @@ class BaseGraph(object):
@cached_property @cached_property
def _len(self): def _len(self):
"""Getter for the maximum series size""" """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 @cached_property
def _min(self): def _min(self):
"""Getter for the minimum series value""" """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 @cached_property
def _max(self): def _max(self):
"""Getter for the maximum series value""" """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 @cached_property
def _order(self): def _order(self):
@ -164,11 +165,16 @@ class BaseGraph(object):
self._split_title() self._split_title()
self._compute_margin() self._compute_margin()
self._decorate() self._decorate()
if self.series and self._has_data():
self._plot() self._plot()
else:
self.svg.draw_no_data()
def _has_data(self): def _has_data(self):
"""Check if there is any data""" """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): def render(self, is_unicode):
"""Render the graph, and return the svg string""" """Render the graph, and return the svg string"""

13
pygal/graph/gauge.py

@ -39,15 +39,15 @@ class Gauge(Graph):
def arc_pos(self, value): def arc_pos(self, value):
aperture = pi / 3 aperture = pi / 3
if value > self._max: if value > self.max_:
return (3 * pi - aperture / 2) / 2 return (3 * pi - aperture / 2) / 2
if value < self._min: if value < self.min_:
return (3 * pi + aperture / 2) / 2 return (3 * pi + aperture / 2) / 2
start = 3 * pi / 2 + aperture / 2 start = 3 * pi / 2 + aperture / 2
return start + (2 * pi - aperture) * ( return start + (2 * pi - aperture) * (
value - self.min_) / (self.max_ - self.min_) value - self.min_) / (self.max_ - self.min_)
def needle(self, serie_node, serie,): def needle(self, serie_node, serie):
thickness = .05 thickness = .05
for i, value in enumerate(serie.values): for i, value in enumerate(serie.values):
if value is None: if value is None:
@ -95,7 +95,8 @@ class Gauge(Graph):
else '')) else ''))
x, y = self.view((.9, theta)) x, y = self.view((.9, theta))
self.svg.node(guides, 'text', self.svg.node(
guides, 'text',
x=x, x=x,
y=y y=y
).text = label ).text = label
@ -109,8 +110,8 @@ class Gauge(Graph):
self._box.xmin = -1 self._box.xmin = -1
self._box.ymin = -1 self._box.ymin = -1
self.min_ = self._min self.min_ = self._min or 0
self.max_ = self._max self.max_ = self._max or 0
if self.max_ - self.min_ == 0: if self.max_ - self.min_ == 0:
self.min_ -= 1 self.min_ -= 1
self.max_ += 1 self.max_ += 1

7
pygal/graph/radar.py

@ -106,7 +106,7 @@ class Radar(Line):
y=y).text = label y=y).text = label
def _compute(self): 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)] x_pos = [.5 * pi + i * delta for i in range(self._len + 1)]
for serie in self.series: for serie in self.series:
serie.points = [ serie.points = [
@ -126,8 +126,9 @@ class Radar(Line):
extended_vals, extended_x_pos, polar=True) extended_vals, extended_x_pos, polar=True)
self._box.margin *= 2 self._box.margin *= 2
self._box.xmin = self._box.ymin = - self._max _max = self._max or 1
self._box.xmax = self._box.ymax = self._rmax = self._max self._box.xmin = self._box.ymin = - _max
self._box.xmax = self._box.ymax = self._rmax = _max
y_pos = compute_scale( y_pos = compute_scale(
0, self._box.ymax, self.logarithmic, self.order_min, max_scale=8 0, self._box.ymax, self.logarithmic, self.order_min, max_scale=8

5
pygal/graph/stackedbar.py

@ -46,9 +46,8 @@ class StackedBar(Bar):
return positive_vals, negative_vals return positive_vals, negative_vals
def _compute_box(self, positive_vals, negative_vals): def _compute_box(self, positive_vals, negative_vals):
self._box.ymin, self._box.ymax = ( self._box.ymin = negative_vals and min(min(negative_vals), self.zero)
min(min(negative_vals), self.zero), self._box.ymax = positive_vals and max(max(positive_vals), self.zero)
max(max(positive_vals), self.zero))
def _compute(self): def _compute(self):
positive_vals, negative_vals = self._get_separated_values() positive_vals, negative_vals = self._get_separated_values()

10
pygal/graph/xy.py

@ -41,29 +41,33 @@ class XY(Line):
for serie in self.series for serie in self.series
for val in serie.values for val in serie.values
if val[1] is not None] if val[1] is not None]
if xvals:
xmin = min(xvals) xmin = min(xvals)
xmax = max(xvals) xmax = max(xvals)
rng = (xmax - xmin) rng = (xmax - xmin)
else:
rng = None
for serie in self.series: for serie in self.series:
serie.points = serie.values serie.points = serie.values
if self.interpolate: if self.interpolate and rng:
vals = zip(*sorted( vals = zip(*sorted(
filter(lambda t: None not in t, filter(lambda t: None not in t,
serie.points), key=lambda x: x[0])) serie.points), key=lambda x: x[0]))
serie.interpolated = self._interpolate( serie.interpolated = self._interpolate(
vals[1], vals[0], xy=True, xy_xmin=xmin, xy_rng=rng) vals[1], vals[0], xy=True, xy_xmin=xmin, xy_rng=rng)
if self.interpolate: if self.interpolate and rng:
xvals = [val[0] xvals = [val[0]
for serie in self.series for serie in self.series
for val in serie.interpolated] for val in serie.interpolated]
yvals = [val[1] yvals = [val[1]
for serie in self.series for serie in self.series
for val in serie.interpolated] for val in serie.interpolated]
if rng:
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 = compute_scale( x_pos = compute_scale(
self._box.xmin, self._box.xmax, self.logarithmic, self.order_min) self._box.xmin, self._box.xmax, self.logarithmic, self.order_min)
y_pos = compute_scale( y_pos = compute_scale(

6
pygal/svg.py

@ -194,9 +194,9 @@ class Svg(object):
self.root.set('height', str(self.graph.height)) self.root.set('height', str(self.graph.height))
def draw_no_data(self): def draw_no_data(self):
no_data = self.node(self.root, 'text', no_data = self.node(self.graph.nodes['text_overlay'], 'text',
x=self.graph.width / 2, x=self.graph.view.width / 2,
y=self.graph.height / 2, y=self.graph.view.height / 2,
class_='no_data') class_='no_data')
no_data.text = self.graph.no_data_text no_data.text = self.graph.no_data_text

4
pygal/test/test_config.py

@ -212,7 +212,7 @@ def test_show_dots():
def test_no_data(): def test_no_data():
line = Line() line = Line()
q = line.render_pyquery() q = line.render_pyquery()
assert q("text").text() == "No data" assert q(".text-overlay text").text() == "No data"
line.no_data_text = u"þæ®þ怀&ij¿’€" line.no_data_text = u"þæ®þ怀&ij¿’€"
q = line.render_pyquery() q = line.render_pyquery()
assert q("text").text() == u"þæ®þ怀&ij¿’€" assert q(".text-overlay text").text() == u"þæ®þ怀&ij¿’€"

16
pygal/test/test_graph.py

@ -95,7 +95,7 @@ def test_empty_lists(Chart):
chart.add('B', []) chart.add('B', [])
chart.x_labels = ('red', 'green', 'blue') chart.x_labels = ('red', 'green', 'blue')
q = chart.render_pyquery() q = chart.render_pyquery()
assert len(q(".legend")) == 1 assert len(q(".legend")) == 2
def test_empty_lists_with_nones(Chart): 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.add('B', [None, 4, 4])
chart.x_labels = ('red', 'green', 'blue') chart.x_labels = ('red', 'green', 'blue')
q = chart.render_pyquery() q = chart.render_pyquery()
assert len(q(".legend")) == 1 assert len(q(".legend")) == 2
def test_non_iterable_value(Chart): def test_non_iterable_value(Chart):
@ -160,14 +160,14 @@ def test_values_by_dict(Chart):
def test_no_data_with_no_values(Chart): def test_no_data_with_no_values(Chart):
chart = Chart() chart = Chart()
q = chart.render_pyquery() 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): def test_no_data_with_empty_serie(Chart):
chart = Chart() chart = Chart()
chart.add('Serie', []) chart.add('Serie', [])
q = chart.render_pyquery() 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): 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('Serie1', [])
chart.add('Serie2', []) chart.add('Serie2', [])
q = chart.render_pyquery() 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): def test_no_data_with_none(Chart):
chart = Chart() chart = Chart()
chart.add('Serie', None) chart.add('Serie', None)
q = chart.render_pyquery() 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): def test_no_data_with_list_of_none(Chart):
chart = Chart() chart = Chart()
chart.add('Serie', [None]) chart.add('Serie', [None])
q = chart.render_pyquery() 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): 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('Serie1', [None, None, None, None])
chart.add('Serie2', [None, None, None]) chart.add('Serie2', [None, None, None])
q = chart.render_pyquery() q = chart.render_pyquery()
assert q("text").text() == "No data" assert q(".text-overlay text").text() == "No data"

6
pygal/test/test_line.py

@ -75,11 +75,9 @@ def test_no_dot():
line = Line() line = Line()
line.add('no dot', []) line.add('no dot', [])
q = line.render_pyquery() q = line.render_pyquery()
assert q(".plot") == [] assert q(".text-overlay text").text() == 'No data'
assert q("text").text() == 'No data'
def test_no_dot_at_all(): def test_no_dot_at_all():
q = Line().render_pyquery() q = Line().render_pyquery()
assert q(".plot") == [] assert q(".text-overlay text").text() == 'No data'
assert q("text").text() == 'No data'

3
pygal/test/test_util.py

@ -73,7 +73,8 @@ def test_format():
obj.a = 1 obj.a = 1
obj.b = True obj.b = True
obj.c = '3' obj.c = '3'
assert template('foo {{ o.a }} {{o.b}}-{{o.c}}', assert template(
'foo {{ o.a }} {{o.b}}-{{o.c}}',
o=obj) == 'foo 1 True-3' o=obj) == 'foo 1 True-3'

2
pygal/util.py

@ -318,8 +318,6 @@ def prepare_values(raw, config, cls):
else: else:
raw_values = list(raw_values) raw_values = list(raw_values)
if not filter(lambda x: x is not None, raw_values):
continue
for index, raw_value in enumerate( for index, raw_value in enumerate(
raw_values + ( raw_values + (
(width - len(raw_values)) * [None] # aligning values (width - len(raw_values)) * [None] # aligning values

44
pygal/view.py

@ -48,10 +48,46 @@ class Box(object):
margin = .02 margin = .02
def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1): def __init__(self, xmin=0, ymin=0, xmax=1, ymax=1):
self.xmin = xmin self._xmin = xmin
self.ymin = ymin self._ymin = ymin
self.xmax = xmax self._xmax = xmax
self.ymax = ymax 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 @property
def width(self): def width(self):

Loading…
Cancel
Save