diff --git a/demo/simple_test.py b/demo/simple_test.py index cf797df..e6e522a 100755 --- a/demo/simple_test.py +++ b/demo/simple_test.py @@ -102,11 +102,11 @@ with open('out-xy.svg', 'w') as f: f.write(xy.render()) pie = Pie(Config(style=NeonStyle)) -pie.add('test', [11]) -# pie.add('test2', [29, 21, 9]) -# pie.add('test3', [24, 10, 32]) -# pie.add('test4', [20, 18, 9]) -# pie.add('test5', [17, 5, 10]) +pie.add('test', [11, 8, 21]) +pie.add('test2', [29, 21, 9]) +pie.add('test3', [24, 10, 32]) +pie.add('test4', [20, 18, 9]) +pie.add('test5', [17, 5, 10]) pie.title = "Pie test" with open('out-pie.svg', 'w') as f: f.write(pie.render()) diff --git a/pygal/config.py b/pygal/config.py index 4c54ded..e67402e 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -34,6 +34,8 @@ class Config(object): y_scale = 1 # If set to a filename, this will replace the default css base_css = None + # or default js + base_js = None # Style holding values injected in css style = DefaultStyle # Various font sizes diff --git a/pygal/css/graph.css b/pygal/css/graph.css index db2ce39..f92ddee 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -55,18 +55,10 @@ svg * { font-size: {{ font_sizes.legend }}; } -.legends .legend rect { - fill-opacity: {{ style.opacity }}; -} - .legends .legend:hover text { stroke: {{ style.foreground_light }}; } -.legends .legend:hover rect { - fill-opacity: 1 -} - .axis text { font-size: {{ font_sizes.label }}; font-family: sans; @@ -117,10 +109,14 @@ svg * { stroke-width: 5px; } -.series .rect, .series .slice { +.reactive { fill-opacity: {{ style.opacity }}; } +.reactive.active { + fill-opacity: {{ fill_opacity_hover }}; +} + .series text { opacity: 0; font-size: {{ font_sizes.values }}; @@ -130,23 +126,13 @@ svg * { text-shadow: 0 0 16px {{ style.background }}; } -.series .dot:hover text, .series .bar:hover text, .series .slice:hover text { +.series text.active { opacity: 1; } -.series .bar:hover .rect, .series .slice:hover path, .series .slice:hover circle{ - fill-opacity: 1; -} - -.series .line { - stroke-width: 1px; - fill-opacity: {{ fill_opacity }}; -} - -.series .line:hover { - stroke-width: 2px; - fill-opacity: {{ fill_opacity_hover }}; -} +/* .series .line { */ +/* fill-opacity: {{ fill_opacity }}; */ +/* } */ diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index ec09c31..22871ea 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -33,9 +33,10 @@ class Bar(Graph): fun = swap if self._horizontal else ident return (self.view(fun(t)), self.view(fun(T))) - bars = self.svg.node(serie_node, class_="bars") + bars = self.svg.node(serie_node['plot'], class_="bars") view_values = map(view, values) for i, ((x, y), (X, Y)) in enumerate(view_values): + tag = '%d_%d' % (serie.index, i) # x and y are left range coords and X, Y right ones if self._horizontal: x, y, X, Y = Y, X, y, x @@ -64,22 +65,27 @@ class Bar(Graph): height = -height bar = self.svg.node(bars, class_='bar') - self.svg.transposable_node(bar, 'rect', - x=x, - y=y - shift, - rx=self.rounded_bars * 1, - ry=self.rounded_bars * 1, - width=bar_inner_width, - height=height, - class_='rect') + self.svg.transposable_node( + bar, 'rect', + x=x, + y=y - shift, + rx=self.rounded_bars * 1, + ry=self.rounded_bars * 1, + width=bar_inner_width, + height=height, + id="active-%s" % tag, + class_='rect reactive') if self._horizontal: x += .3 * self.values_font_size y += height / 2 else: y += height / 2 + .3 * self.values_font_size - self.svg.transposable_node(bar, 'text', - x=x + bar_inner_width / 2, - y=y - shift, + self.svg.transposable_node( + bar, 'text', + x=x + bar_inner_width / 2, + y=y - shift, + id="reactive-%s" % tag, + class_='reactive-text' ).text = str(values[i][1][1]) return stack_vals diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 4a57bc2..eeb3011 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -36,6 +36,10 @@ class Graph(BaseGraph): x=0, y=0, width=self.view.width, height=self.view.height) + self.overlay = self.svg.node( + self.graph_node, class_="plot overlay", + transform="translate(%d, %d)" % ( + self.margin.left, self.margin.top)) def _x_axis(self): if not self._x_labels: @@ -102,11 +106,12 @@ class Graph(BaseGraph): for i, title in enumerate(self._legends): legend = self.svg.node(legends, class_='legend') self.svg.node(legend, 'rect', - x=0, - y=1.5 * i * self.legend_box_size, - width=self.legend_box_size, - height=self.legend_box_size, - class_="color-%d" % i, + x=0, + y=1.5 * i * self.legend_box_size, + width=self.legend_box_size, + height=self.legend_box_size, + class_="color-%d activate-serie reactive" % i, + id="activate-serie-%d" % i ).text = title # Serious magical numbers here self.svg.node(legend, 'text', @@ -124,5 +129,9 @@ class Graph(BaseGraph): ).text = self.title def _serie(self, serie): - return self.svg.node( - self.plot, class_='series serie-%d color-%d' % (serie, serie)) + return dict( + plot=self.svg.node( + self.plot, class_='series serie-%d color-%d' % (serie, serie)), + overlay=self.svg.node( + self.overlay, class_='series serie-%d color-%d' % ( + serie, serie))) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 3210c99..0469db2 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -46,11 +46,16 @@ class Line(Graph): view_values = map(self.view, serie.points) if self.show_dots: - dots = self.svg.node(serie_node, class_="dots") + dots = self.svg.node(serie_node['overlay'], class_="dots") for i, (x, y) in enumerate(view_values): dot = self.svg.node(dots, class_='dot') - self.svg.node(dot, 'circle', cx=x, cy=y, r=2.5) - self.svg.node(dot, 'text', x=x, y=y + tag = '%d_%d' % (serie.index, i) + self.svg.node(dot, 'circle', cx=x, cy=y, r=2.5, + id="active-%s" % tag, + class_='reactive') + self.svg.node(dot, 'text', x=x, y=y, + id="reactive-%s" % tag, + class_='reactive-text' ).text = self._get_value(serie.points, i) if self.stroke: @@ -59,7 +64,8 @@ class Line(Graph): if self.fill: view_values = self._fill(view_values) self.svg.line( - serie_node, view_values, class_='line') + serie_node['plot'], view_values, + class_='line reactive') def _compute(self): self._x_pos = [x / float(self._len - 1) for x in range(self._len) diff --git a/pygal/graph/pie.py b/pygal/graph/pie.py index 1afe9fa..e43bfb2 100644 --- a/pygal/graph/pie.py +++ b/pygal/graph/pie.py @@ -23,9 +23,9 @@ from math import cos, sin, pi class Pie(Graph): """Pie graph""" - def slice(self, serie_node, start_angle, angle, perc, + def slice(self, serie_node, start_angle, angle, perc, tag, small=False): - slices = self.svg.node(serie_node, class_="slices") + slices = self.svg.node(serie_node['plot'], class_="slices") slice_ = self.svg.node(slices, class_="slice") center = ((self.width - self.margin.x) / 2., (self.height - self.margin.y) / 2.) @@ -38,7 +38,8 @@ class Pie(Graph): cx=center[0], cy=center[1], r=r, - class_='slice') + id="active-%s" % tag, + class_='slice reactive') else: rxy = '%f %f' % tuple([r] * 2) to = '%f %f' % (r * sin(angle), r * (1 - cos(angle))) @@ -48,14 +49,17 @@ class Pie(Graph): rxy, 1 if angle > pi else 0, to), + id="active-%s" % tag, transform='rotate(%f %s)' % ( start_angle * 180 / pi, center_str), - class_='slice') + class_='slice reactive') text_angle = pi / 2. - (start_angle + angle / 2.) - text_r = min(center) * .8 - self.svg.node(slice_, 'text', - x=center[0] + text_r * cos(text_angle), - y=center[1] - text_r * sin(text_angle), + text_r = r * .8 + self.svg.node(serie_node['overlay'], 'text', + x=center[0] + text_r * cos(text_angle), + y=center[1] - text_r * sin(text_angle), + id="reactive-%s" % tag, + class_='reactive-text' ).text = '{:.2%}'.format(perc) def _compute(self): @@ -74,14 +78,17 @@ class Pie(Graph): self.slice( self._serie(serie.index), current_angle, - angle, sum(serie.values) / total) + angle, sum(serie.values) / total, + '%d' % serie.index) if len(serie.values) > 1: small_current_angle = current_angle - for val in serie.values: + for i, val in enumerate(serie.values): small_angle = 2 * pi * val / total self.slice( self._serie(serie.index), small_current_angle, - small_angle, val / total, True) + small_angle, val / total, + '%d_%d' % (serie.index, i), + True) small_current_angle += small_angle current_angle += angle diff --git a/pygal/js/graph.coffee b/pygal/js/graph.coffee new file mode 100644 index 0000000..bc62b8f --- /dev/null +++ b/pygal/js/graph.coffee @@ -0,0 +1,57 @@ +_ = (x) -> document.querySelectorAll(x) + +add_class = (e, class_name) -> + return if not e + cn = e.getAttribute('class').split(' ') + if class_name not in cn + cn.push(class_name) + e.setAttribute('class', cn.join(' ')) + +rm_class = (e, class_name) -> + return if not e + cn = e.getAttribute('class').split(' ') + for cls, i in cn + if cls == class_name + cn.splice(i, 1) + e.setAttribute('class', cn.join(' ')) + + +@svg_load = -> + for element in _('.reactive-text') + element.addEventListener('mouseover', ((e) -> + -> + add_class(e, 'active') + add_class(document.getElementById(e.id.replace(/re/, '')), 'active') + )(element), false) + element.addEventListener('mouseout', ((e) -> + -> + rm_class(e, 'active') + rm_class(document.getElementById(e.id.replace(/re/, '')), 'active') + )(element), false) + for element in _('.reactive') + element.addEventListener('mouseover', ((e) -> + -> + add_class(e, 'active') + add_class(document.getElementById('re' + e.id), 'active') + )(element), false) + element.addEventListener('mouseout', ((e) -> + -> + rm_class(e, 'active') + rm_class(document.getElementById('re' + e.id), 'active') + )(element), false) + + for element in _('.activate-serie') + element.addEventListener('mouseover', ((e) -> + -> + num = e.id.replace('activate-serie-', '') + for element in _('.serie-' + num + ' .reactive') + add_class(element, 'active') + add_class(document.getElementById('re' + element.id), 'active') + )(element), false) + element.addEventListener('mouseout', ((e) -> + -> + num = e.id.replace('activate-serie-', '') + for element in _('.serie-' + num + ' .reactive') + rm_class(element, 'active') + rm_class(document.getElementById('re' + element.id), 'active') + )(element), false) diff --git a/pygal/js/graph.js b/pygal/js/graph.js new file mode 100644 index 0000000..4c04dee --- /dev/null +++ b/pygal/js/graph.js @@ -0,0 +1,99 @@ +// Generated by CoffeeScript 1.2.1-pre +(function() { + var add_class, rm_class, _, + __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; + + _ = function(x) { + return document.querySelectorAll(x); + }; + + add_class = function(e, class_name) { + var cn; + if (!e) return; + cn = e.getAttribute('class').split(' '); + if (__indexOf.call(cn, class_name) < 0) cn.push(class_name); + return e.setAttribute('class', cn.join(' ')); + }; + + rm_class = function(e, class_name) { + var cls, cn, i, _i, _len; + if (!e) return; + cn = e.getAttribute('class').split(' '); + for (i = _i = 0, _len = cn.length; _i < _len; i = ++_i) { + cls = cn[i]; + if (cls === class_name) cn.splice(i, 1); + } + return e.setAttribute('class', cn.join(' ')); + }; + + this.svg_load = function() { + var element, _i, _j, _k, _len, _len2, _len3, _ref, _ref2, _ref3, _results; + _ref = _('.reactive-text'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + element = _ref[_i]; + element.addEventListener('mouseover', (function(e) { + return function() { + add_class(e, 'active'); + return add_class(document.getElementById(e.id.replace(/re/, '')), 'active'); + }; + })(element), false); + element.addEventListener('mouseout', (function(e) { + return function() { + rm_class(e, 'active'); + return rm_class(document.getElementById(e.id.replace(/re/, '')), 'active'); + }; + })(element), false); + } + _ref2 = _('.reactive'); + for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { + element = _ref2[_j]; + element.addEventListener('mouseover', (function(e) { + return function() { + add_class(e, 'active'); + return add_class(document.getElementById('re' + e.id), 'active'); + }; + })(element), false); + element.addEventListener('mouseout', (function(e) { + return function() { + rm_class(e, 'active'); + return rm_class(document.getElementById('re' + e.id), 'active'); + }; + })(element), false); + } + _ref3 = _('.activate-serie'); + _results = []; + for (_k = 0, _len3 = _ref3.length; _k < _len3; _k++) { + element = _ref3[_k]; + element.addEventListener('mouseover', (function(e) { + return function() { + var element, num, _l, _len4, _ref4, _results2; + num = e.id.replace('activate-serie-', ''); + _ref4 = _('.serie-' + num + ' .reactive'); + _results2 = []; + for (_l = 0, _len4 = _ref4.length; _l < _len4; _l++) { + element = _ref4[_l]; + add_class(element, 'active'); + _results2.push(add_class(document.getElementById('re' + element.id), 'active')); + } + return _results2; + }; + })(element), false); + _results.push(element.addEventListener('mouseout', (function(e) { + return function() { + var element, num, _l, _len4, _ref4, _results2; + num = e.id.replace('activate-serie-', ''); + _ref4 = _('.serie-' + num + ' .reactive'); + _results2 = []; + for (_l = 0, _len4 = _ref4.length; _l < _len4; _l++) { + element = _ref4[_l]; + rm_class(element, 'active'); + _results2.push(rm_class(document.getElementById('re' + element.id), 'active')); + } + return _results2; + }; + })(element), false)); + } + return _results; + }; + +}).call(this); diff --git a/pygal/svg.py b/pygal/svg.py index 51f6a2d..990fd99 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -35,12 +35,15 @@ class Svg(object): nsmap={ None: self.ns, 'xlink': 'http://www.w3.org/1999/xlink', - }) + }, + onload="svg_load();") self.root.append(etree.Comment(u'Generated with pygal ©Kozea 2012')) self.root.append(etree.Comment(u'http://github.com/Kozea/pygal')) self.defs = self.node(tag='defs') self.add_style(self.graph.base_css or os.path.join( os.path.dirname(__file__), 'css', 'graph.css')) + self.add_script(self.graph.base_js or os.path.join( + os.path.dirname(__file__), 'js', 'graph.js')) def add_style(self, css): style = self.node(self.defs, 'style', type='text/css') @@ -56,6 +59,11 @@ class Svg(object): if self.graph.fill else 0) style.text = templ.decode('utf-8') + def add_script(self, js): + script = self.node(self.root, 'script', type='text/javascript') + with open(js) as f: + script.text = f.read() + def node(self, parent=None, tag='g', attrib=None, **extras): if parent is None: parent = self.root diff --git a/relauncher b/relauncher index 4ea9119..1fc768b 100755 --- a/relauncher +++ b/relauncher @@ -1,6 +1,10 @@ #!/bin/zsh +pkill -f livereload +pkill -f reload +pkill -f SimpleHTTPServer + livereload& -reload ./demo/simple_test.py **/*.py& +reload ./demo/simple_test.py **/*.{py,js,css}& python -m SimpleHTTPServer 1515& sleep 1 chromium http://localhost:1515/&