Browse Source

Add the ability to edit node attribute. Partly Fix #223 and fix #184

pull/242/head
Florian Mounier 10 years ago
parent
commit
be367ae139
  1. 10
      demo/moulinrouge/tests.py
  2. 6
      pygal/graph/bar.py
  3. 28
      pygal/graph/box.py
  4. 7
      pygal/graph/dot.py
  5. 6
      pygal/graph/funnel.py
  6. 7
      pygal/graph/gauge.py
  7. 7
      pygal/graph/histogram.py
  8. 6
      pygal/graph/line.py
  9. 6
      pygal/graph/pie.py
  10. 9
      pygal/graph/treemap.py
  11. 11
      pygal/svg.py
  12. 7
      pygal/test/test_graph.py
  13. 14
      pygal/util.py

10
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()

6
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))

28
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(

7
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',
alter(self.svg.node(
dots, 'circle',
cx=x, cy=y, r=size,
class_='dot reactive tooltip-trigger' + (
' negative' if value < 0 else ''))
' negative' if value < 0 else '')), metadata)
value = self._format(value)
self._tooltip_data(dots, value, x, y, classes='centered')

6
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

7
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)

7
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))

6
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(

6
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

9
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',
alter(
self.svg.node(
rect, 'rect',
x=rx,
y=ry,
width=rw,
height=rh,
class_='rect reactive tooltip-trigger')
class_='rect reactive tooltip-trigger'),
metadata)
self._tooltip_data(rect, value,
rx + rw / 2,

11
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,7 +244,8 @@ class Svg(object):
diff(center, project(rho, theta)))
if angle == 2 * pi:
self.node(node, 'circle',
rv = self.node(
node, 'circle',
cx=center[0],
cy=center[1],
r=radius,
@ -254,19 +255,23 @@ class Svg(object):
absolute_project(radius, start_angle + angle),
absolute_project(small_radius, start_angle + angle),
absolute_project(small_radius, start_angle)]
self.node(node, 'path',
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"""

7
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'))

14
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)

Loading…
Cancel
Save