diff --git a/pygal/config.py b/pygal/config.py index b0f93d0..26f2dfd 100644 --- a/pygal/config.py +++ b/pygal/config.py @@ -44,7 +44,8 @@ class Config(object): style = DefaultStyle # Various font sizes label_font_size = 10 - values_font_size = 12 + value_font_size = 8 + tooltip_font_size = 20 title_font_size = 16 legend_font_size = 14 # Specify labels rotation angles in degrees @@ -92,12 +93,13 @@ class Config(object): """Can be updated with kwargs""" self.__dict__.update(kwargs) - @property - def font_sizes(self): + def font_sizes(self, with_unit=True): fs = FontSizes() for name in dir(self): if name.endswith('_font_size'): - setattr(fs, - name.replace('_font_size', ''), - '%dpx' % getattr(self, name)) + setattr( + fs, + name.replace('_font_size', ''), + ('%dpx' % getattr(self, name) + ) if with_unit else getattr(self, name)) return fs diff --git a/pygal/css/graph.css b/pygal/css/graph.css index 9518c22..04d92c2 100644 --- a/pygal/css/graph.css +++ b/pygal/css/graph.css @@ -58,12 +58,11 @@ text.no_data { .legends .legend text { font-family: monospace; font-size: {{ font_sizes.legend }}; - stroke: {{ style.foreground }}; fill: {{ style.foreground }}; + fill-opacity: 1; } .legends .legend:hover text { - stroke: {{ style.foreground_light }}; fill: {{ style.foreground_light }}; } @@ -94,7 +93,6 @@ text.no_data { stroke: {{ style.foreground_light }}; } - .axis .guide.line { stroke: {{ style.foreground_dark }}; stroke-dasharray: 4,4; @@ -104,6 +102,11 @@ text.no_data { stroke: {{ style.foreground }}; stroke-dasharray: 6,6; } +.axis text.major { + stroke-width: 0.5px; + stroke: {{ style.foreground_light }}; + fill: {{ style.foreground_light }}; +} .axis.{{ hidden }} .guide.line { opacity: 0; @@ -122,15 +125,6 @@ text.no_data { opacity: 1; } -.dot circle { - stroke-width: 1px; - fill-opacity: 1; -} - -.dot circle.active { - stroke-width: 5px; -} - .nofill { fill: none; } @@ -143,18 +137,35 @@ text.no_data { fill-opacity: {{ style.opacity_hover }}; } +.dot { + stroke-width: 1px; + fill-opacity: 1; +} + +.dot.active { + stroke-width: 5px; +} + .series text { - opacity: 0; - font-size: {{ font_sizes.values }}; - text-anchor: middle; - stroke: {{ style.foreground_light }}; + font-size: {{ font_sizes.value }}; + stroke: none; fill: {{ style.foreground_light }}; - text-shadow: 0 0 16px {{ style.background }}; } .series text.active { opacity: 1; } +#tooltip rect { + fill-opacity: 0.8; + fill: {{ style.background }}; + stroke: {{ style.foreground_light }}; +} + +#tooltip text { + fill-opacity: 1; + fill: {{ style.foreground_light }}; + font-size: {{ font_sizes.tooltip }}; +} {{ style.colors }} diff --git a/pygal/graph/bar.py b/pygal/graph/bar.py index 0e94c23..a158762 100644 --- a/pygal/graph/bar.py +++ b/pygal/graph/bar.py @@ -76,10 +76,10 @@ class Bar(Graph): id="active-%s" % tag, class_='rect reactive') if self._horizontal: - x += .3 * self.values_font_size + x += .3 * self.value_font_size y += height / 2 else: - y += height / 2 + .3 * self.values_font_size + y += height / 2 + .3 * self.value_font_size self.svg.transposable_node( serie_node['overlay'], 'text', x=x + bar_inner_width / 2, diff --git a/pygal/graph/graph.py b/pygal/graph/graph.py index 13014e5..145a99e 100644 --- a/pygal/graph/graph.py +++ b/pygal/graph/graph.py @@ -41,6 +41,20 @@ class Graph(BaseGraph): self.graph_node, class_="plot overlay", transform="translate(%d, %d)" % ( self.margin.left, self.margin.top)) + self.text_overlay = self.svg.node( + self.graph_node, class_="plot text-overlay", + transform="translate(%d, %d)" % ( + self.margin.left, self.margin.top)) + tooltip_overlay = self.svg.node( + self.graph_node, class_="tooltip-overlay", + transform="translate(%d, %d)" % ( + self.margin.left, self.margin.top)) + self.tooltip_node = self.svg.node(tooltip_overlay, id="tooltip") + self.svg.node(self.tooltip_node, 'rect', + id="tooltip-box", + rx=5, ry=5, + ) + self.svg.node(self.tooltip_node, 'text') def _x_axis(self): if not self._x_labels: @@ -140,7 +154,11 @@ class Graph(BaseGraph): def _serie(self, serie): return dict( plot=self.svg.node( - self.plot, class_='series serie-%d color-%d' % (serie, serie)), + self.plot, + class_='series serie-%d color-%d' % (serie, serie)), overlay=self.svg.node( - self.overlay, class_='series serie-%d color-%d' % ( - serie, serie))) + self.overlay, + class_='series serie-%d color-%d' % (serie, serie)), + text_overlay=self.svg.node( + self.text_overlay, + class_='series serie-%d color-%d' % (serie, serie))) diff --git a/pygal/graph/line.py b/pygal/graph/line.py index 6f6668d..c32e2e9 100644 --- a/pygal/graph/line.py +++ b/pygal/graph/line.py @@ -48,15 +48,14 @@ class Line(Graph): if self.show_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') - 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) + val = self._get_value(serie.points, i) + self.svg.node(dots, 'circle', cx=x, cy=y, r=2.5, + class_='dot reactive tooltip-trigger') + self.svg.node(dots, 'desc').text = val + self.svg.node(serie_node['text_overlay'], 'text', + x=x + self.value_font_size, + y=y + self.value_font_size, + ).text = val if self.stroke: if self.interpolate: diff --git a/pygal/js/graph.coffee b/pygal/js/graph.coffee index b8eba2f..455a909 100644 --- a/pygal/js/graph.coffee +++ b/pygal/js/graph.coffee @@ -1,4 +1,9 @@ _ = (x) -> document.querySelectorAll(x) +__ = (x) -> document.getElementById(x) +padding = 5 +tooltip_timeout = 0 +tooltip_font_size = parseInt("{{ font_sizes.tooltip }}") + add_class = (e, class_name) -> return if not e @@ -15,6 +20,8 @@ rm_class = (e, class_name) -> cn.splice(i, 1) e.setAttribute('class', cn.join(' ')) +svg = (tag) -> document.createElementNS('http://www.w3.org/2000/svg', tag) + activate = (elements...) -> for element in elements add_class(element, 'active') @@ -36,15 +43,44 @@ hover = (elts, over, out) -> elt.addEventListener('mouseover', over.bind(elt) , false) elt.addEventListener('mouseout', out.bind(elt) , false) +tooltip = (elt) -> + clearTimeout(tooltip_timeout) + _tooltip = __('tooltip') + _text = _tooltip.getElementsByTagName('text')[0] + _rect = _tooltip.getElementsByTagName('rect')[0] + _text.textContent = elt.nextElementSibling.textContent + w = _text.offsetWidth + 2 * padding + h = _text.offsetHeight + 2 * padding + _rect.setAttribute('width', w) + _rect.setAttribute('height', h) + _text.setAttribute('x', padding) + _text.setAttribute('y', padding + tooltip_font_size) + x = elt.getAttribute('cx') || elt.getAttribute('x') + y = elt.getAttribute('cy') || elt.getAttribute('y') + if x - w > 0 + x -= w + if y - h > 0 + y -= h + _tooltip.setAttribute('transform', "translate(#{x} #{y})") + +untooltip = -> + tooltip_timeout = setTimeout (-> + __('tooltip').setAttribute('transform', 'translate(-100000, -100000)')), 1000 + @svg_load = -> + for text in _('.text-overlay .series') + text.setAttribute('display', 'none') hover _('.reactive-text'), (-> activate(@, active(@))), (-> deactivate(@, active(@))) hover _('.reactive'), (-> activate(@, reactive(@))), (-> deactivate(@, reactive(@))) hover _('.activate-serie'), ( -> num = this.id.replace('activate-serie-', '') + _('.text-overlay .serie-' + num)[0].setAttribute('display', 'inline') for element in _('.serie-' + num + ' .reactive') activate(element, reactive(element))), ( -> num = this.id.replace('activate-serie-', '') + _('.text-overlay .serie-' + num)[0].setAttribute('display', 'none') for element in _('.serie-' + num + ' .reactive') deactivate(element, reactive(element))) + hover _('.tooltip-trigger'), (-> tooltip(@)), (-> untooltip()) diff --git a/pygal/js/graph.js b/pygal/js/graph.js index 1c75190..5ce2cec 100644 --- a/pygal/js/graph.js +++ b/pygal/js/graph.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript 1.2.1-pre (function() { - var activate, active, add_class, deactivate, hover, reactive, rm_class, _, + var activate, active, add_class, deactivate, hover, padding, reactive, rm_class, svg, tooltip, tooltip_font_size, tooltip_timeout, untooltip, _, __, __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; }, __slice = [].slice; @@ -8,6 +8,16 @@ return document.querySelectorAll(x); }; + __ = function(x) { + return document.getElementById(x); + }; + + padding = 5; + + tooltip_timeout = 0; + + tooltip_font_size = parseInt("{{ font_sizes.tooltip }}"); + add_class = function(e, class_name) { var cn; if (!e) return; @@ -27,6 +37,10 @@ return e.setAttribute('class', cn.join(' ')); }; + svg = function(tag) { + return document.createElementNS('http://www.w3.org/2000/svg', tag); + }; + activate = function() { var element, elements, _i, _len, _results; elements = 1 <= arguments.length ? __slice.call(arguments, 0) : []; @@ -76,7 +90,39 @@ return _results; }; + tooltip = function(elt) { + var h, w, x, y, _rect, _text, _tooltip; + clearTimeout(tooltip_timeout); + _tooltip = __('tooltip'); + _text = _tooltip.getElementsByTagName('text')[0]; + _rect = _tooltip.getElementsByTagName('rect')[0]; + _text.textContent = elt.nextElementSibling.textContent; + w = _text.offsetWidth + 2 * padding; + h = _text.offsetHeight + 2 * padding; + _rect.setAttribute('width', w); + _rect.setAttribute('height', h); + _text.setAttribute('x', padding); + _text.setAttribute('y', padding + tooltip_font_size); + x = elt.getAttribute('cx') || elt.getAttribute('x'); + y = elt.getAttribute('cy') || elt.getAttribute('y'); + if (x - w > 0) x -= w; + if (y - h > 0) y -= h; + return _tooltip.setAttribute('transform', "translate(" + x + " " + y + ")"); + }; + + untooltip = function() { + return tooltip_timeout = setTimeout((function() { + return __('tooltip').setAttribute('transform', 'translate(-100000, -100000)'); + }), 1000); + }; + this.svg_load = function() { + var text, _i, _len, _ref; + _ref = _('.text-overlay .series'); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + text = _ref[_i]; + text.setAttribute('display', 'none'); + } hover(_('.reactive-text'), (function() { return activate(this, active(this)); }), (function() { @@ -87,27 +133,34 @@ }), (function() { return deactivate(this, reactive(this)); })); - return hover(_('.activate-serie'), (function() { - var element, num, _i, _len, _ref, _results; + hover(_('.activate-serie'), (function() { + var element, num, _j, _len2, _ref2, _results; num = this.id.replace('activate-serie-', ''); - _ref = _('.serie-' + num + ' .reactive'); + _('.text-overlay .serie-' + num)[0].setAttribute('display', 'inline'); + _ref2 = _('.serie-' + num + ' .reactive'); _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - element = _ref[_i]; + for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { + element = _ref2[_j]; _results.push(activate(element, reactive(element))); } return _results; }), (function() { - var element, num, _i, _len, _ref, _results; + var element, num, _j, _len2, _ref2, _results; num = this.id.replace('activate-serie-', ''); - _ref = _('.serie-' + num + ' .reactive'); + _('.text-overlay .serie-' + num)[0].setAttribute('display', 'none'); + _ref2 = _('.serie-' + num + ' .reactive'); _results = []; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - element = _ref[_i]; + for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) { + element = _ref2[_j]; _results.push(deactivate(element, reactive(element))); } return _results; })); + return hover(_('.tooltip-trigger'), (function() { + return tooltip(this); + }), (function() { + return untooltip(); + })); }; }).call(this); diff --git a/pygal/svg.py b/pygal/svg.py index 335ec5d..f9a11da 100644 --- a/pygal/svg.py +++ b/pygal/svg.py @@ -51,14 +51,17 @@ class Svg(object): templ = template( f.read(), style=self.graph.style, - font_sizes=self.graph.font_sizes, + font_sizes=self.graph.font_sizes(), hidden='y' if self.graph._horizontal else 'x') 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() + templ = template( + f.read(), + font_sizes=self.graph.font_sizes(False)) + script.text = templ.decode('utf-8') def node(self, parent=None, tag='g', attrib=None, **extras): if parent is None: