From be367ae13939d6ad43c160d567cf330ed09c1cac Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Fri, 26 Jun 2015 15:11:08 +0200 Subject: [PATCH] Add the ability to edit node attribute. Partly Fix #223 and fix #184 --- demo/moulinrouge/tests.py | 10 +++++++--- pygal/graph/bar.py | 6 +++--- pygal/graph/box.py | 28 ++++++++++++++-------------- pygal/graph/dot.py | 11 ++++++----- pygal/graph/funnel.py | 6 +++--- pygal/graph/gauge.py | 7 ++++--- pygal/graph/histogram.py | 7 ++++--- pygal/graph/line.py | 6 +++--- pygal/graph/pie.py | 6 +++--- pygal/graph/treemap.py | 17 ++++++++++------- pygal/svg.py | 31 ++++++++++++++++++------------- pygal/test/test_graph.py | 7 ++++--- pygal/util.py | 14 +++++++++----- 13 files changed, 88 insertions(+), 68 deletions(-) diff --git a/demo/moulinrouge/tests.py b/demo/moulinrouge/tests.py index 1c34e29..51ebcb1 100644 --- a/demo/moulinrouge/tests.py +++ b/demo/moulinrouge/tests.py @@ -619,11 +619,15 @@ def get_test_routes(app): def test_custom_metadata_for(chart): c = CHARTS_BY_NAME[chart]() c.add('1', [ - {'style': 'fill: red', 'value': 1}, - {'color': 'blue', 'value': 2}, + {'style': 'fill: red', 'value': 1, 'node': {'r': 12}}, + {'color': 'blue', 'value': 2, 'node': {'width': 12}}, {'style': 'fill: red; stroke: yellow', 'value': 3}]) c.add('2', [ - {'value': 4}, + {'value': 4, 'xlink': { + 'href': 'javascript:alert("-")', 'target': 'top', + 'class': 'lol' + } + }, {'color': 'green', 'value': 5}, 6]) return c.render_response() diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 5ba98b8..8c9c747 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -23,7 +23,7 @@ Bar chart from __future__ import division from pygal.graph.graph import Graph -from pygal.util import swap, ident, compute_scale, decorate +from pygal.util import swap, ident, compute_scale, decorate, alter class Bar(Graph): @@ -54,10 +54,10 @@ class Bar(Graph): width -= 2 * serie_margin height = self.view.y(zero) - y r = serie.rounded_bars * 1 if serie.rounded_bars else 0 - self.svg.transposable_node( + alter(self.svg.transposable_node( parent, 'rect', x=x, y=y, rx=r, ry=r, width=width, height=height, - class_='rect reactive tooltip-trigger') + class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) transpose = swap if self.horizontal else ident return transpose((x + width / 2, y + height / 2)) diff --git a/pygal/graph/box.py b/pygal/graph/box.py index fc99434..ab48cb3 100644 --- a/pygal/graph/box.py +++ b/pygal/graph/box.py @@ -22,7 +22,7 @@ Box plot from __future__ import division from pygal.graph.graph import Graph -from pygal.util import compute_scale, decorate +from pygal.util import compute_scale, decorate, alter from pygal._compat import is_list_like from bisect import bisect_left, bisect_right @@ -119,12 +119,12 @@ class Box(Graph): metadata) val = self._format(serie.values) - x_center, y_center = self._draw_box(box, serie.values[1:6], - serie.outliers, serie.index) + 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._static_value(serie_node, val, x_center, y_center) - def _draw_box(self, parent_node, quartiles, outliers, box_index): + def _draw_box(self, parent_node, quartiles, outliers, box_index, metadata): """ Return the center of a bounding box defined by a box plot. Draws a box plot on self.svg. @@ -141,46 +141,46 @@ class Box(Graph): shift = (width - whisker_width) / 2 xs = left_edge + shift xe = left_edge + width - shift - self.svg.line( + alter(self.svg.line( parent_node, coords=[(xs, self.view.y(whisker)), (xe, self.view.y(whisker))], class_='reactive tooltip-trigger', - attrib={'stroke-width': 3}) + attrib={'stroke-width': 3}), metadata) # draw lines connecting whiskers to box (Q1 and Q3) - self.svg.line( + alter(self.svg.line( parent_node, coords=[(left_edge + width / 2, self.view.y(quartiles[0])), (left_edge + width / 2, self.view.y(quartiles[1]))], class_='reactive tooltip-trigger', - attrib={'stroke-width': 2}) - self.svg.line( + attrib={'stroke-width': 2}), metadata) + alter(self.svg.line( parent_node, coords=[(left_edge + width / 2, self.view.y(quartiles[4])), (left_edge + width / 2, self.view.y(quartiles[3]))], class_='reactive tooltip-trigger', - attrib={'stroke-width': 2}) + attrib={'stroke-width': 2}), metadata) # box, bounded by Q1 and Q3 - self.svg.node( + alter(self.svg.node( parent_node, tag='rect', x=left_edge, y=self.view.y(quartiles[1]), height=self.view.y(quartiles[3]) - self.view.y(quartiles[1]), width=width, - class_='subtle-fill reactive tooltip-trigger') + class_='subtle-fill reactive tooltip-trigger'), metadata) # draw outliers for o in outliers: - self.svg.node( + alter(self.svg.node( parent_node, tag='circle', cx=left_edge+width/2, cy=self.view.y(o), r=3, - class_='subtle-fill reactive tooltip-trigger') + class_='subtle-fill reactive tooltip-trigger'), metadata) return (left_edge + width / 2, self.view.y( diff --git a/pygal/graph/dot.py b/pygal/graph/dot.py index e330ad4..8f6cd64 100644 --- a/pygal/graph/dot.py +++ b/pygal/graph/dot.py @@ -22,7 +22,7 @@ Dot chart """ from __future__ import division -from pygal.util import decorate, cut, safe_enumerate, cached_property +from pygal.util import decorate, cut, safe_enumerate, cached_property, alter from pygal.graph.graph import Graph from pygal.view import View, ReverseView from math import log10 @@ -57,10 +57,11 @@ class Dot(Graph): self.svg, self.svg.node(serie_node['plot'], class_="dots"), metadata) - self.svg.node(dots, 'circle', - cx=x, cy=y, r=size, - class_='dot reactive tooltip-trigger' + ( - ' negative' if value < 0 else '')) + alter(self.svg.node( + dots, 'circle', + cx=x, cy=y, r=size, + class_='dot reactive tooltip-trigger' + ( + ' negative' if value < 0 else '')), metadata) value = self._format(value) self._tooltip_data(dots, value, x, y, classes='centered') diff --git a/pygal/graph/funnel.py b/pygal/graph/funnel.py index 37ebd12..13609b9 100644 --- a/pygal/graph/funnel.py +++ b/pygal/graph/funnel.py @@ -22,7 +22,7 @@ Funnel chart """ from __future__ import division -from pygal.util import decorate, cut, compute_scale +from pygal.util import decorate, cut, compute_scale, alter from pygal.adapters import positive, none_to_zero from pygal.graph.graph import Graph @@ -48,10 +48,10 @@ class Funnel(Graph): self.svg.node(serie_node['plot'], class_="funnels"), metadata) - self.svg.node( + alter(self.svg.node( funnels, 'polygon', points=' '.join(map(fmt, map(self.view, poly))), - class_='funnel reactive tooltip-trigger') + class_='funnel reactive tooltip-trigger'), metadata) x, y = self.view(( self._x_labels[serie.index][1], # Poly center from label diff --git a/pygal/graph/gauge.py b/pygal/graph/gauge.py index 17d5c06..75c256c 100644 --- a/pygal/graph/gauge.py +++ b/pygal/graph/gauge.py @@ -22,7 +22,7 @@ Gauge chart """ from __future__ import division -from pygal.util import decorate, compute_scale +from pygal.util import decorate, compute_scale, alter from pygal.view import PolarThetaView, PolarThetaLogView from pygal.graph.graph import Graph @@ -54,13 +54,14 @@ class Gauge(Graph): self.svg.node(serie_node['plot'], class_="dots"), metadata) - self.svg.node( + alter(self.svg.node( gauges, 'polygon', points=' '.join([ fmt(self.view((0, 0))), fmt(self.view((.75, theta))), fmt(self.view((.8, theta))), fmt(self.view((.75, theta)))]), - class_='line reactive tooltip-trigger') + class_='line reactive tooltip-trigger'), + metadata) x, y = self.view((.75, theta)) self._tooltip_data(gauges, value, x, y) diff --git a/pygal/graph/histogram.py b/pygal/graph/histogram.py index 76fedb8..d10d4fd 100644 --- a/pygal/graph/histogram.py +++ b/pygal/graph/histogram.py @@ -23,7 +23,8 @@ Histogram chart from __future__ import division from pygal.graph.graph import Graph -from pygal.util import swap, ident, compute_scale, decorate, cached_property +from pygal.util import ( + swap, ident, compute_scale, decorate, cached_property, alter) class Histogram(Graph): @@ -73,10 +74,10 @@ class Histogram(Graph): width -= 2 * series_margin r = serie.rounded_bars * 1 if serie.rounded_bars else 0 - self.svg.transposable_node( + alter(self.svg.transposable_node( parent, 'rect', x=x, y=y, rx=r, ry=r, width=width, height=height, - class_='rect reactive tooltip-trigger') + class_='rect reactive tooltip-trigger'), serie.metadata.get(i)) transpose = swap if self.horizontal else ident return transpose((x + width / 2, y + height / 2)) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index d368e9b..ab36baa 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -22,7 +22,7 @@ Line chart """ from __future__ import division from pygal.graph.graph import Graph -from pygal.util import cached_property, compute_scale, decorate +from pygal.util import cached_property, compute_scale, decorate, alter class Line(Graph): @@ -109,9 +109,9 @@ class Line(Graph): self.svg.node(serie_node['overlay'], class_="dots"), metadata) val = self._get_value(serie.points, i) - self.svg.transposable_node( + alter(self.svg.transposable_node( dots, 'circle', cx=x, cy=y, r=serie.dots_size, - class_='dot reactive tooltip-trigger') + class_='dot reactive tooltip-trigger'), metadata) self._tooltip_data( dots, val, x, y) self._static_value( diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index e3a9104..42aee13 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -22,7 +22,7 @@ Pie chart """ from __future__ import division -from pygal.util import decorate +from pygal.util import decorate, alter from pygal.graph.graph import Graph from pygal.adapters import positive, none_to_zero from math import pi @@ -79,9 +79,9 @@ class Pie(Graph): big_radius = radius * .9 small_radius = radius * serie.inner_radius - self.svg.slice( + alter(self.svg.slice( serie_node, slice_, big_radius, small_radius, - angle, start_angle, center, val) + angle, start_angle, center, val), metadata) start_angle += angle total_perc += perc diff --git a/pygal/graph/treemap.py b/pygal/graph/treemap.py index 0be375d..9bad71c 100644 --- a/pygal/graph/treemap.py +++ b/pygal/graph/treemap.py @@ -22,7 +22,7 @@ Treemap chart """ from __future__ import division -from pygal.util import decorate, cut +from pygal.util import decorate, cut, alter from pygal.graph.graph import Graph from pygal.adapters import positive, none_to_zero @@ -46,12 +46,15 @@ class Treemap(Graph): self.svg.node(rects, class_="rect"), metadata) - self.svg.node(rect, 'rect', - x=rx, - y=ry, - width=rw, - height=rh, - class_='rect reactive tooltip-trigger') + alter( + self.svg.node( + rect, 'rect', + x=rx, + y=ry, + width=rw, + height=rh, + class_='rect reactive tooltip-trigger'), + metadata) self._tooltip_data(rect, value, rx + rw / 2, diff --git a/pygal/svg.py b/pygal/svg.py index c0a5375..b5db166 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -228,7 +228,7 @@ class Svg(object): line = ' '.join([coord_format(c) for c in coords[origin_index + 1:] if None not in c]) - self.node(node, 'path', + return self.node(node, 'path', d=root % (origin, line), **kwargs) def slice( @@ -244,29 +244,34 @@ class Svg(object): diff(center, project(rho, theta))) if angle == 2 * pi: - self.node(node, 'circle', - cx=center[0], - cy=center[1], - r=radius, - class_='slice reactive tooltip-trigger') + rv = self.node( + node, 'circle', + cx=center[0], + cy=center[1], + r=radius, + class_='slice reactive tooltip-trigger') elif angle > 0: to = [absolute_project(radius, start_angle), absolute_project(radius, start_angle + angle), absolute_project(small_radius, start_angle + angle), absolute_project(small_radius, start_angle)] - self.node(node, 'path', - d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( - to[0], - get_radius(radius), int(angle > pi), to[1], - to[2], - get_radius(small_radius), int(angle > pi), to[3]), - class_='slice reactive tooltip-trigger') + rv = self.node( + node, 'path', + d='M%s A%s 0 %d 1 %s L%s A%s 0 %d 0 %s z' % ( + to[0], + get_radius(radius), int(angle > pi), to[1], + to[2], + get_radius(small_radius), int(angle > pi), to[3]), + class_='slice reactive tooltip-trigger') + else: + rv = None x, y = diff(center, project( (radius + small_radius) / 2, start_angle + angle / 2)) self.graph._tooltip_data(node, val, x, y, classes="centered") if angle >= 0.3: # 0.3 radians is about 17 degrees self.graph._static_value(serie_node, val, x, y) + return rv def pre_render(self): """Last things to do before rendering""" diff --git a/pygal/test/test_graph.py b/pygal/test/test_graph.py index f9d8899..1b992a9 100644 --- a/pygal/test/test_graph.py +++ b/pygal/test/test_graph.py @@ -96,11 +96,12 @@ def test_metadata(Chart): 'target': '_blank'}, 'label': 'Seven'} ]) q = chart.render_pyquery() - for md in ( - 'Three', 'http://4.example.com/', - 'Five', 'http://7.example.com/', 'Seven'): + for md in ('Three', 'Five', 'Seven'): assert md in cut(q('desc'), 'text') + for md in ('http://7.example.com/', 'http://4.example.com/'): + assert md in [e.attrib.get('xlink:href') for e in q('a')] + if Chart in (pygal.Pie, pygal.Treemap): # Slices with value 0 are not rendered assert len(v) - 1 == len(q('.tooltip-trigger').siblings('.value')) diff --git a/pygal/util.py b/pygal/util.py index 69d1f39..7ccf1ee 100644 --- a/pygal/util.py +++ b/pygal/util.py @@ -241,15 +241,19 @@ def decorate(svg, node, metadata): if 'style' in metadata: node.attrib['style'] = metadata.pop('style') - for key, value in metadata.items(): - if key == 'xlink' and isinstance(value, dict): - value = value.get('href', value) - if value: - svg.node(node, 'desc', class_=key).text = to_unicode(value) + if 'label' in metadata: + svg.node(node, 'desc', class_='label').text = to_unicode( + metadata['label']) return node +def alter(node, metadata): + if node and metadata and 'node' in metadata: + node.attrib.update( + dict((k, str(v)) for k, v in metadata['node'].items())) + + def cycle_fill(short_list, max_len): """Fill a list to max_len using a cycle of it""" short_list = list(short_list)