diff --git a/demo/moulinrouge/__init__.py b/demo/moulinrouge/__init__.py index 298d2eb..635aa92 100644 --- a/demo/moulinrouge/__init__.py +++ b/demo/moulinrouge/__init__.py @@ -213,7 +213,7 @@ def create_app(): else: config.x_labels = [random_label() for i in range(data)] svgs.append({'type': type, - 'series': xy_series if type == 'XY' else other_series, + 'series': xy_series if chart._dual else other_series, 'config': b64encode(pickle.dumps(config))}) return render_template('svgs.jinja2', diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 7e63a5f..73ddaaa 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -376,8 +376,8 @@ def get_test_routes(app): chart.x_label_rotation = 25 chart.y_label_rotation = 50 chart.add('1', [30, 20, -2]) - chart.add(10 * '1b', [-4, 50, 6], secondary=True) - chart.add(10 * '2b', [None, 10, 20], secondary=True) + chart.add('1b', [-4, 50, 6], secondary=True) + chart.add('2b', [None, 10, 20], secondary=True) chart.add('2', [8, 21, -0]) chart.add('3', [None, 20, 10]) chart.add('3b', [-1, 2, -3], secondary=True) @@ -395,6 +395,8 @@ def get_test_routes(app): @app.route('/test/box') def test_box(): chart = Box() + chart.js = ('http://l:2343/2.0.x/pygal-tooltips.js',) + chart.box_mode = '1.5IQR' chart.add('One', [15, 8, 2, -12, 9, 23]) chart.add('Two', [5, 8, 2, -9, 23, 12]) chart.add('Three', [8, -2, 12, -5, 9, 3]) @@ -408,6 +410,7 @@ def get_test_routes(app): stacked = StackedLine(stack_from_top=True, logarithmic=True) stacked.add('1', [1, 2]) stacked.add('2', [10, 12]) + stacked.x_labels = ['a', 'b', 'c', 'd'] return stacked.render_response() @app.route('/test/stacked/reverse') @@ -415,7 +418,6 @@ def get_test_routes(app): stacked = StackedBar(stack_from_top=True) stacked.add('1', [1, 2, 3]) stacked.add('2', [4, 5, 6]) - stacked.x_labels = ['a', 'b', 'c'] return stacked.render_response() @app.route('/test/show_dots') diff --git a/docs/api/pygal.adapters.rst b/docs/api/pygal.adapters.rst index ee9be39..e69de29 100644 --- a/docs/api/pygal.adapters.rst +++ b/docs/api/pygal.adapters.rst @@ -1,7 +0,0 @@ -pygal.adapters module -===================== - -.. automodule:: pygal.adapters - :members: - :undoc-members: - :show-inheritance: diff --git a/pygal/adapters.py b/pygal/adapters.py index 3720c63..04186d0 100644 --- a/pygal/adapters.py +++ b/pygal/adapters.py @@ -18,12 +18,15 @@ # along with pygal. If not, see . """Value adapters to use when a chart doesn't accept all value types""" from decimal import Decimal +from pygal._compat import is_str def positive(x): """Return zero if value is negative""" if x is None: return + if is_str(x): + return x if x < 0: return 0 return x diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index f43c509..8e56719 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -88,7 +88,8 @@ class Bar(Graph): x_center, y_center = self._bar( serie, bar, x, y, i, self.zero, secondary=rescale) self._tooltip_data( - bar, val, x_center, y_center, classes="centered") + bar, val, x_center, y_center, "centered", + self._get_x_label(i)) self._static_value(serie_node, val, x_center, y_center) def _compute(self): @@ -98,11 +99,13 @@ class Bar(Graph): if self._max: self._box.ymax = max(self._max, self.zero) - x_pos = [ + self._x_pos = [ x / self._len for x in range(self._len + 1) ] if self._len > 1 else [0, 1] # Center if only one value - self._points(x_pos) + self._points(self._x_pos) + + self._x_pos = [(i + .5) / self._len for i in range(self._len)] def _compute_secondary(self): """Compute parameters for secondary series rendering""" diff --git a/pygal/graph/base.py b/pygal/graph/base.py index 7b3abf8..7848fb3 100644 --- a/pygal/graph/base.py +++ b/pygal/graph/base.py @@ -149,6 +149,8 @@ class BaseGraph(object): value = (None, None, None) elif not is_list_like(value): value = (value, self.zero, self.zero) + elif len(value) == 2: + value = (1, value[0], value[1]) value = list(map(adapter, value)) elif self._dual: if value is None: diff --git a/pygal/graph/box.py b/pygal/graph/box.py index 5f368c5..2629f4e 100644 --- a/pygal/graph/box.py +++ b/pygal/graph/box.py @@ -50,14 +50,16 @@ class Box(Graph): def format_maybe_quartile(x): if is_list_like(x): if self.box_mode == "extremes": - return 'Min: %s Q1: %s Q2: %s Q3: %s Max: %s' \ - % tuple(map(sup, x[1:6])) + return ( + 'Min: %s\nQ1 : %s\nQ2 : %s\nQ3 : %s\nMax: %s' % + tuple(map(sup, x[1:6]))) elif self.box_mode in ["tukey", "stdev", "pstdev"]: - return 'Min: %s Lower Whisker: %s Q1: %s Q2: %s Q3: %s '\ - 'Upper Whisker: %s Max: %s' % tuple(map(sup, x)) + return ( + 'Min: %s\nLower Whisker: %s\nQ1: %s\nQ2: %s\nQ3: %s\n' + 'Upper Whisker: %s\nMax: %s' % tuple(map(sup, x))) elif self.box_mode == '1.5IQR': # 1.5IQR mode - return 'Q1: %s Q2: %s Q3: %s' % tuple(map(sup, x[2:5])) + return 'Q1: %s\nQ2: %s\nQ3: %s' % tuple(map(sup, x[2:5])) else: return sup(x) return format_maybe_quartile @@ -71,15 +73,14 @@ class Box(Graph): serie.values, serie.outliers = \ self._box_points(serie.values, self.box_mode) + self._x_pos = [ + (i + .5) / self._order for i in range(self._order)] + if self._min: self._box.ymin = min(self._min, self.zero) if self._max: self._box.ymax = max(self._max, self.zero) - def _compute_x_labels(self): - self._x_labels = self.x_labels and list(zip(self.x_labels, [ - (i + .5) / self._order for i in range(self._order)])) - def _plot(self): """Plot the series data""" for serie in self.series: @@ -108,7 +109,8 @@ class Box(Graph): x_center, y_center = self._draw_box( box, serie.values[1:6], serie.outliers, serie.index, metadata) - self._tooltip_data(box, val, x_center, y_center, classes="centered") + self._tooltip_data(box, val, x_center, y_center, "centered", + self._get_x_label(serie.index)) self._static_value(serie_node, val, x_center, y_center) def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata): diff --git a/pygal/graph/dot.py b/pygal/graph/dot.py index 21e9dcb..733fceb 100644 --- a/pygal/graph/dot.py +++ b/pygal/graph/dot.py @@ -68,7 +68,9 @@ class Dot(Graph): ' negative' if value < 0 else '')), metadata) value = self._format(value) - self._tooltip_data(dots, value, x, y, classes='centered') + self._tooltip_data( + dots, value, x, y, 'centered', + self._get_x_label(i)) self._static_value(serie_node, value, x, y) def _compute(self): @@ -86,10 +88,6 @@ class Dot(Graph): (self._x_pos[i], self._y_pos[j]) for i in range(x_len)] - def _compute_x_labels(self): - self._x_labels = self.x_labels and list( - zip(self.x_labels, self._x_pos)) - def _compute_y_labels(self): self._y_labels = list(zip( self.y_labels or [ diff --git a/pygal/graph/dual.py b/pygal/graph/dual.py index 266e828..6ed3be0 100644 --- a/pygal/graph/dual.py +++ b/pygal/graph/dual.py @@ -51,3 +51,7 @@ class Dual(Graph): else: self._x_labels = list(zip(map(self._x_format, x_pos), x_pos)) + + def _get_x_label(self, i): + """Convenience function to get the x_label of a value index""" + return diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py index 98135b3..73b47d8 100644 --- a/pygal/graph/funnel.py +++ b/pygal/graph/funnel.py @@ -53,11 +53,13 @@ class Funnel(Graph): points=' '.join(map(fmt, map(self.view, poly))), class_='funnel reactive tooltip-trigger'), metadata) + # Poly center from label x, y = self.view(( - # Poly center from label self._center(self._x_pos[serie.index]), sum([point[1] for point in poly]) / len(poly))) - self._tooltip_data(funnels, value, x, y, classes='centered') + self._tooltip_data( + funnels, value, x, y, 'centered', + self._get_x_label(serie.index)) self._static_value(serie_node, value, x, y) def _center(self, x): diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py index b6f1044..01a3060 100644 --- a/pygal/graph/gauge.py +++ b/pygal/graph/gauge.py @@ -86,7 +86,9 @@ class Gauge(Graph): metadata) x, y = self.view((.75, theta)) - self._tooltip_data(gauges, value, x, y) + self._tooltip_data( + gauges, value, x, y, + xlabel=self._get_x_label(i)) self._static_value(serie_node, value, x, y) def _y_axis(self, draw_axes=True): @@ -138,6 +140,9 @@ class Gauge(Graph): self.min_, self.max_) + def _compute_x_labels(self): + pass + def _compute_y_labels(self): y_pos = compute_scale( self.min_, self.max_, self.logarithmic, diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index b11ec97..53b7ca7 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -429,7 +429,7 @@ class Graph(PublicApi): if y is not None else None) for x, y in points] - def _tooltip_data(self, node, value, x, y, classes=None): + def _tooltip_data(self, node, value, x, y, classes=None, xlabel=None): """Insert in desc tags informations for the javascript tooltip""" self.svg.node(node, 'desc', class_="value").text = value if classes is None: @@ -444,6 +444,9 @@ class Graph(PublicApi): class_="x " + classes).text = str(x) self.svg.node(node, 'desc', class_="y " + classes).text = str(y) + if xlabel: + self.svg.node(node, 'desc', + class_="x_label").text = str(xlabel) def _static_value(self, serie_node, value, x, y): """Write the print value""" @@ -499,6 +502,12 @@ class Graph(PublicApi): """Hook called after compute and before margin computations and plot""" pass + def _get_x_label(self, i): + """Convenience function to get the x_label of a value index""" + if not self.x_labels or not self._x_labels or len(self._x_labels) <= i: + return + return self._x_labels[i][0] + @property def all_series(self): """Getter for all series (nomal and secondary)""" @@ -716,8 +725,8 @@ class Graph(PublicApi): ) def _compute_x_labels(self): - self._x_labels = self.x_labels and list(zip(self.x_labels, [ - (i + .5) / self._len for i in range(self._len)])) + self._x_labels = self.x_labels and list( + zip(self.x_labels, self._x_pos)) def _compute_y_labels(self): y_pos = compute_scale( diff --git a/pygal/graph/histogram.py b/pygal/graph/histogram.py index 4933674..5e10514 100644 --- a/pygal/graph/histogram.py +++ b/pygal/graph/histogram.py @@ -22,16 +22,14 @@ as bars of varying width. """ from __future__ import division -from pygal._compat import is_list_like + from pygal.graph.dual import Dual -from pygal.util import ( - swap, ident, compute_scale, decorate, cached_property, alter) +from pygal.util import alter, cached_property, decorate, ident, swap class Histogram(Dual): """Histogram chart class""" - _series_margin = 0 @cached_property @@ -97,7 +95,8 @@ class Histogram(Dual): x_center, y_center = self._bar( serie, bar, x0, x1, y, i, self.zero, secondary=rescale) self._tooltip_data( - bar, val, x_center, y_center, classes="centered") + bar, val, x_center, y_center, "centered", + self._get_x_label(i)) self._static_value(serie_node, val, x_center, y_center) def _compute(self): diff --git a/pygal/graph/horizontal.py b/pygal/graph/horizontal.py index d482481..74694a4 100644 --- a/pygal/graph/horizontal.py +++ b/pygal/graph/horizontal.py @@ -53,3 +53,9 @@ class HorizontalGraph(Graph): self.width - self.margin_box.x, self.height - self.margin_box.y, self._box) + + def _get_x_label(self, i): + """Convenience function to get the x_label of a value index""" + if not self.x_labels or not self._y_labels or len(self._y_labels) <= i: + return + return self._y_labels[i][0] diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 1cd47e5..38d448f 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -120,7 +120,8 @@ class Line(Graph): dots, 'circle', cx=x, cy=y, r=serie.dots_size, class_='dot reactive tooltip-trigger'), metadata) self._tooltip_data( - dots, val, x, y) + dots, val, x, y, + xlabel=self._get_x_label(i)) self._static_value( serie_node, val, x + self.style.value_font_size, @@ -141,11 +142,11 @@ class Line(Graph): def _compute(self): """Compute y min and max and y scale and set labels""" # X Labels - x_pos = [ + self._x_pos = [ x / (self._len - 1) for x in range(self._len) ] if self._len != 1 else [.5] # Center if only one value - self._points(x_pos) + self._points(self._x_pos) if self.include_x_axis: # Y Label diff --git a/pygal/graph/map.py b/pygal/graph/map.py index 690e967..72cac70 100644 --- a/pygal/graph/map.py +++ b/pygal/graph/map.py @@ -121,7 +121,7 @@ class BaseMap(Graph): alter(node, metadata) value = self._get_value((area_code, value)) - self._tooltip_data(area, value, 0, 0, classes='auto') + self._tooltip_data(area, value, 0, 0, 'auto') self.nodes['plot'].append(map) diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index cc11336..edd8fc7 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -82,7 +82,7 @@ class Pie(Graph): alter(self.svg.slice( serie_node, slice_, big_radius, small_radius, - angle, start_angle, center, val), metadata) + angle, start_angle, center, val, i), metadata) start_angle += angle total_perc += perc @@ -91,7 +91,7 @@ class Pie(Graph): self.svg.slice(serie_node, self.svg.node(slices, class_="big_slice"), radius * .9, 0, serie_angle, - original_start_angle, center, val) + original_start_angle, center, val, i) return serie_angle def _compute_x_labels(self): diff --git a/pygal/graph/radar.py b/pygal/graph/radar.py index 040bbfa..0815e85 100644 --- a/pygal/graph/radar.py +++ b/pygal/graph/radar.py @@ -39,7 +39,6 @@ class Radar(Line): def __init__(self, *args, **kwargs): """Init custom vars""" - self._x_pos = None self._rmax = None super(Radar, self).__init__(*args, **kwargs) @@ -168,14 +167,14 @@ class Radar(Line): def _compute(self): """Compute r min max and labels position""" delta = 2 * pi / self._len if self._len else 0 - x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] + self._x_pos = [.5 * pi + i * delta for i in range(self._len + 1)] for serie in self.all_series: serie.points = [ - (v, x_pos[i]) + (v, self._x_pos[i]) for i, v in enumerate(serie.values)] if self.interpolate: extended_x_pos = ( - [.5 * pi - delta] + x_pos) + [.5 * pi - delta] + self._x_pos) extended_vals = (serie.values[-1:] + serie.values) serie.interpolated = list( @@ -190,11 +189,6 @@ class Radar(Line): self._rmax = self._max or 1 self._box.set_polar_box(self._rmin, self._rmax) self._self_close = True - self._x_pos = x_pos - - def _compute_x_labels(self): - self._x_labels = self.x_labels and list( - zip(self.x_labels, self._x_pos)) def _compute_y_labels(self): y_pos = compute_scale( diff --git a/pygal/graph/stackedbar.py b/pygal/graph/stackedbar.py index aca0b57..5b61e4f 100644 --- a/pygal/graph/stackedbar.py +++ b/pygal/graph/stackedbar.py @@ -65,11 +65,11 @@ class StackedBar(Bar): positive_vals = positive_vals or [self.zero] negative_vals = negative_vals or [self.zero] - x_pos = [ + self._x_pos = [ x / self._len for x in range(self._len + 1) ] if self._len > 1 else [0, 1] # Center if only one value - self._points(x_pos) + self._points(self._x_pos) self.negative_cumulation = [0] * self._len self.positive_cumulation = [0] * self._len @@ -82,6 +82,8 @@ class StackedBar(Bar): self.secondary_positive_cumulation = [0] * self._len self._pre_compute_secondary(positive_vals, negative_vals) + self._x_pos = [(i + .5) / self._len for i in range(self._len)] + def _pre_compute_secondary(self, positive_vals, negative_vals): """Compute secondary y min and max""" self._secondary_min = (negative_vals and min( diff --git a/pygal/graph/time.py b/pygal/graph/time.py index 879b2f3..1ad22f8 100644 --- a/pygal/graph/time.py +++ b/pygal/graph/time.py @@ -25,7 +25,7 @@ into float for xy plot and back to their type for display from pygal.adapters import positive from pygal.graph.xy import XY from datetime import datetime, date, time, timedelta -from pygal._compat import timestamp, total_seconds +from pygal._compat import timestamp, total_seconds, is_str def datetime_to_timestamp(x): @@ -70,6 +70,8 @@ def time_to_seconds(x): ((x.hour * 60) + x.minute) * 60 + x.second ) * 10 ** 6 + x.microsecond) / 10 ** 6 + if is_str(x): + return x # Clamp to valid time return x and max(0, min(x, 24 * 3600 - 10 ** -6)) diff --git a/pygal/graph/treemap.py b/pygal/graph/treemap.py index bab2615..81ee7b1 100644 --- a/pygal/graph/treemap.py +++ b/pygal/graph/treemap.py @@ -55,13 +55,16 @@ class Treemap(Graph): class_='rect reactive tooltip-trigger'), metadata) - self._tooltip_data(rect, value, - rx + rw / 2, - ry + rh / 2, - classes='centered') - self._static_value(serie_node, value, - rx + rw / 2, - ry + rh / 2) + self._tooltip_data( + rect, value, + rx + rw / 2, + ry + rh / 2, + 'centered', + self._get_x_label(i)) + self._static_value( + serie_node, value, + rx + rw / 2, + ry + rh / 2) def _binary_tree(self, data, total, x, y, w, h, parent=None): if total == 0: @@ -115,6 +118,12 @@ class Treemap(Graph): self._binary_tree( half2, half2_sum, x + x_pivot, y, w - x_pivot, h, parent) + def _compute_x_labels(self): + pass + + def _compute_y_labels(self): + pass + def _plot(self): total = sum(map(sum, map(lambda x: x.values, self.series))) if total == 0: diff --git a/pygal/svg.py b/pygal/svg.py index f75d61d..137c93e 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -143,7 +143,7 @@ class Svg(object): dct = get_js_dict() # Config adds - dct['legends'] = self.graph._legends + dct['legends'] = self.graph._legends + self.graph._secondary_legends common_script.text = " = ".join( ("window.config", json.dumps( @@ -239,7 +239,7 @@ class Svg(object): def slice( self, serie_node, node, radius, small_radius, - angle, start_angle, center, val): + angle, start_angle, center, val, i): """Draw a pie slice""" project = lambda rho, alpha: ( rho * sin(-alpha), rho * cos(-alpha)) @@ -274,7 +274,9 @@ class Svg(object): x, y = diff(center, project( (radius + small_radius) / 2, start_angle + angle / 2)) - self.graph._tooltip_data(node, val, x, y, classes="centered") + self.graph._tooltip_data( + node, val, x, y, "centered", + self.graph._x_labels and self.graph._x_labels[i][0]) if angle >= 0.3: # 0.3 radians is about 17 degrees self.graph._static_value(serie_node, val, x, y) return rv