Browse Source

Add tooltips, working for lines

pull/8/head
Florian Mounier 13 years ago
parent
commit
7f16330de9
  1. 14
      pygal/config.py
  2. 45
      pygal/css/graph.css
  3. 4
      pygal/graph/bar.py
  4. 24
      pygal/graph/graph.py
  5. 17
      pygal/graph/line.py
  6. 36
      pygal/js/graph.coffee
  7. 73
      pygal/js/graph.js
  8. 7
      pygal/svg.py

14
pygal/config.py

@ -44,7 +44,8 @@ class Config(object):
style = DefaultStyle style = DefaultStyle
# Various font sizes # Various font sizes
label_font_size = 10 label_font_size = 10
values_font_size = 12 value_font_size = 8
tooltip_font_size = 20
title_font_size = 16 title_font_size = 16
legend_font_size = 14 legend_font_size = 14
# Specify labels rotation angles in degrees # Specify labels rotation angles in degrees
@ -92,12 +93,13 @@ class Config(object):
"""Can be updated with kwargs""" """Can be updated with kwargs"""
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
@property def font_sizes(self, with_unit=True):
def font_sizes(self):
fs = FontSizes() fs = FontSizes()
for name in dir(self): for name in dir(self):
if name.endswith('_font_size'): if name.endswith('_font_size'):
setattr(fs, setattr(
name.replace('_font_size', ''), fs,
'%dpx' % getattr(self, name)) name.replace('_font_size', ''),
('%dpx' % getattr(self, name)
) if with_unit else getattr(self, name))
return fs return fs

45
pygal/css/graph.css

@ -58,12 +58,11 @@ text.no_data {
.legends .legend text { .legends .legend text {
font-family: monospace; font-family: monospace;
font-size: {{ font_sizes.legend }}; font-size: {{ font_sizes.legend }};
stroke: {{ style.foreground }};
fill: {{ style.foreground }}; fill: {{ style.foreground }};
fill-opacity: 1;
} }
.legends .legend:hover text { .legends .legend:hover text {
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
} }
@ -94,7 +93,6 @@ text.no_data {
stroke: {{ style.foreground_light }}; stroke: {{ style.foreground_light }};
} }
.axis .guide.line { .axis .guide.line {
stroke: {{ style.foreground_dark }}; stroke: {{ style.foreground_dark }};
stroke-dasharray: 4,4; stroke-dasharray: 4,4;
@ -104,6 +102,11 @@ text.no_data {
stroke: {{ style.foreground }}; stroke: {{ style.foreground }};
stroke-dasharray: 6,6; stroke-dasharray: 6,6;
} }
.axis text.major {
stroke-width: 0.5px;
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }};
}
.axis.{{ hidden }} .guide.line { .axis.{{ hidden }} .guide.line {
opacity: 0; opacity: 0;
@ -122,15 +125,6 @@ text.no_data {
opacity: 1; opacity: 1;
} }
.dot circle {
stroke-width: 1px;
fill-opacity: 1;
}
.dot circle.active {
stroke-width: 5px;
}
.nofill { .nofill {
fill: none; fill: none;
} }
@ -143,18 +137,35 @@ text.no_data {
fill-opacity: {{ style.opacity_hover }}; fill-opacity: {{ style.opacity_hover }};
} }
.dot {
stroke-width: 1px;
fill-opacity: 1;
}
.dot.active {
stroke-width: 5px;
}
.series text { .series text {
opacity: 0; font-size: {{ font_sizes.value }};
font-size: {{ font_sizes.values }}; stroke: none;
text-anchor: middle;
stroke: {{ style.foreground_light }};
fill: {{ style.foreground_light }}; fill: {{ style.foreground_light }};
text-shadow: 0 0 16px {{ style.background }};
} }
.series text.active { .series text.active {
opacity: 1; 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 }} {{ style.colors }}

4
pygal/graph/bar.py

@ -76,10 +76,10 @@ class Bar(Graph):
id="active-%s" % tag, id="active-%s" % tag,
class_='rect reactive') class_='rect reactive')
if self._horizontal: if self._horizontal:
x += .3 * self.values_font_size x += .3 * self.value_font_size
y += height / 2 y += height / 2
else: else:
y += height / 2 + .3 * self.values_font_size y += height / 2 + .3 * self.value_font_size
self.svg.transposable_node( self.svg.transposable_node(
serie_node['overlay'], 'text', serie_node['overlay'], 'text',
x=x + bar_inner_width / 2, x=x + bar_inner_width / 2,

24
pygal/graph/graph.py

@ -41,6 +41,20 @@ class Graph(BaseGraph):
self.graph_node, class_="plot overlay", self.graph_node, class_="plot overlay",
transform="translate(%d, %d)" % ( transform="translate(%d, %d)" % (
self.margin.left, self.margin.top)) 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): def _x_axis(self):
if not self._x_labels: if not self._x_labels:
@ -140,7 +154,11 @@ class Graph(BaseGraph):
def _serie(self, serie): def _serie(self, serie):
return dict( return dict(
plot=self.svg.node( 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( overlay=self.svg.node(
self.overlay, class_='series serie-%d color-%d' % ( self.overlay,
serie, serie))) class_='series serie-%d color-%d' % (serie, serie)),
text_overlay=self.svg.node(
self.text_overlay,
class_='series serie-%d color-%d' % (serie, serie)))

17
pygal/graph/line.py

@ -48,15 +48,14 @@ class Line(Graph):
if self.show_dots: if self.show_dots:
dots = self.svg.node(serie_node['overlay'], class_="dots") dots = self.svg.node(serie_node['overlay'], class_="dots")
for i, (x, y) in enumerate(view_values): for i, (x, y) in enumerate(view_values):
dot = self.svg.node(dots, class_='dot') val = self._get_value(serie.points, i)
tag = '%d_%d' % (serie.index, i) self.svg.node(dots, 'circle', cx=x, cy=y, r=2.5,
self.svg.node(dot, 'circle', cx=x, cy=y, r=2.5, class_='dot reactive tooltip-trigger')
id="active-%s" % tag, self.svg.node(dots, 'desc').text = val
class_='reactive') self.svg.node(serie_node['text_overlay'], 'text',
self.svg.node(dot, 'text', x=x, y=y, x=x + self.value_font_size,
id="reactive-%s" % tag, y=y + self.value_font_size,
class_='reactive-text' ).text = val
).text = self._get_value(serie.points, i)
if self.stroke: if self.stroke:
if self.interpolate: if self.interpolate:

36
pygal/js/graph.coffee

@ -1,4 +1,9 @@
_ = (x) -> document.querySelectorAll(x) _ = (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) -> add_class = (e, class_name) ->
return if not e return if not e
@ -15,6 +20,8 @@ rm_class = (e, class_name) ->
cn.splice(i, 1) cn.splice(i, 1)
e.setAttribute('class', cn.join(' ')) e.setAttribute('class', cn.join(' '))
svg = (tag) -> document.createElementNS('http://www.w3.org/2000/svg', tag)
activate = (elements...) -> activate = (elements...) ->
for element in elements for element in elements
add_class(element, 'active') add_class(element, 'active')
@ -36,15 +43,44 @@ hover = (elts, over, out) ->
elt.addEventListener('mouseover', over.bind(elt) , false) elt.addEventListener('mouseover', over.bind(elt) , false)
elt.addEventListener('mouseout', out.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 = -> @svg_load = ->
for text in _('.text-overlay .series')
text.setAttribute('display', 'none')
hover _('.reactive-text'), (-> activate(@, active(@))), (-> deactivate(@, active(@))) hover _('.reactive-text'), (-> activate(@, active(@))), (-> deactivate(@, active(@)))
hover _('.reactive'), (-> activate(@, reactive(@))), (-> deactivate(@, reactive(@))) hover _('.reactive'), (-> activate(@, reactive(@))), (-> deactivate(@, reactive(@)))
hover _('.activate-serie'), ( hover _('.activate-serie'), (
-> ->
num = this.id.replace('activate-serie-', '') num = this.id.replace('activate-serie-', '')
_('.text-overlay .serie-' + num)[0].setAttribute('display', 'inline')
for element in _('.serie-' + num + ' .reactive') for element in _('.serie-' + num + ' .reactive')
activate(element, reactive(element))), ( activate(element, reactive(element))), (
-> ->
num = this.id.replace('activate-serie-', '') num = this.id.replace('activate-serie-', '')
_('.text-overlay .serie-' + num)[0].setAttribute('display', 'none')
for element in _('.serie-' + num + ' .reactive') for element in _('.serie-' + num + ' .reactive')
deactivate(element, reactive(element))) deactivate(element, reactive(element)))
hover _('.tooltip-trigger'), (-> tooltip(@)), (-> untooltip())

73
pygal/js/graph.js

@ -1,6 +1,6 @@
// Generated by CoffeeScript 1.2.1-pre // Generated by CoffeeScript 1.2.1-pre
(function() { (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; }, __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; __slice = [].slice;
@ -8,6 +8,16 @@
return document.querySelectorAll(x); 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) { add_class = function(e, class_name) {
var cn; var cn;
if (!e) return; if (!e) return;
@ -27,6 +37,10 @@
return e.setAttribute('class', cn.join(' ')); return e.setAttribute('class', cn.join(' '));
}; };
svg = function(tag) {
return document.createElementNS('http://www.w3.org/2000/svg', tag);
};
activate = function() { activate = function() {
var element, elements, _i, _len, _results; var element, elements, _i, _len, _results;
elements = 1 <= arguments.length ? __slice.call(arguments, 0) : []; elements = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
@ -76,7 +90,39 @@
return _results; 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() { 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() { hover(_('.reactive-text'), (function() {
return activate(this, active(this)); return activate(this, active(this));
}), (function() { }), (function() {
@ -87,27 +133,34 @@
}), (function() { }), (function() {
return deactivate(this, reactive(this)); return deactivate(this, reactive(this));
})); }));
return hover(_('.activate-serie'), (function() { hover(_('.activate-serie'), (function() {
var element, num, _i, _len, _ref, _results; var element, num, _j, _len2, _ref2, _results;
num = this.id.replace('activate-serie-', ''); num = this.id.replace('activate-serie-', '');
_ref = _('.serie-' + num + ' .reactive'); _('.text-overlay .serie-' + num)[0].setAttribute('display', 'inline');
_ref2 = _('.serie-' + num + ' .reactive');
_results = []; _results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
element = _ref[_i]; element = _ref2[_j];
_results.push(activate(element, reactive(element))); _results.push(activate(element, reactive(element)));
} }
return _results; return _results;
}), (function() { }), (function() {
var element, num, _i, _len, _ref, _results; var element, num, _j, _len2, _ref2, _results;
num = this.id.replace('activate-serie-', ''); num = this.id.replace('activate-serie-', '');
_ref = _('.serie-' + num + ' .reactive'); _('.text-overlay .serie-' + num)[0].setAttribute('display', 'none');
_ref2 = _('.serie-' + num + ' .reactive');
_results = []; _results = [];
for (_i = 0, _len = _ref.length; _i < _len; _i++) { for (_j = 0, _len2 = _ref2.length; _j < _len2; _j++) {
element = _ref[_i]; element = _ref2[_j];
_results.push(deactivate(element, reactive(element))); _results.push(deactivate(element, reactive(element)));
} }
return _results; return _results;
})); }));
return hover(_('.tooltip-trigger'), (function() {
return tooltip(this);
}), (function() {
return untooltip();
}));
}; };
}).call(this); }).call(this);

7
pygal/svg.py

@ -51,14 +51,17 @@ class Svg(object):
templ = template( templ = template(
f.read(), f.read(),
style=self.graph.style, style=self.graph.style,
font_sizes=self.graph.font_sizes, font_sizes=self.graph.font_sizes(),
hidden='y' if self.graph._horizontal else 'x') hidden='y' if self.graph._horizontal else 'x')
style.text = templ.decode('utf-8') style.text = templ.decode('utf-8')
def add_script(self, js): def add_script(self, js):
script = self.node(self.root, 'script', type='text/javascript') script = self.node(self.root, 'script', type='text/javascript')
with open(js) as f: 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): def node(self, parent=None, tag='g', attrib=None, **extras):
if parent is None: if parent is None:

Loading…
Cancel
Save