From 767de20f4ecfc69de7b6845882ba3eb1dce4f1ae Mon Sep 17 00:00:00 2001 From: Gabriel Date: Sun, 28 Apr 2013 19:02:07 +0200 Subject: [PATCH] Adds Twitter Bootstrap tooltips Twitter bootstrap tooltips for the lava flows, #3 --- coffee/eruptions.coffee | 4 + coffee/lavaLayer.coffee | 16 +- css/barchart.css | 6 + index.html | 1 + js/eruptions.js | 6 +- js/lavaLayer.js | 6 +- js/vendor/bootstrap-tooltip.js | 361 +++++++++++++++++++++++++++++++++ 7 files changed, 391 insertions(+), 9 deletions(-) create mode 100755 js/vendor/bootstrap-tooltip.js diff --git a/coffee/eruptions.coffee b/coffee/eruptions.coffee index 2561611..7981be5 100644 --- a/coffee/eruptions.coffee +++ b/coffee/eruptions.coffee @@ -249,6 +249,10 @@ @lavaLayer.data(boundingBox, lavaFiltered) @lavaLayer.draw() @map.refresh(); + # activate tooltips on the body! (wouldn't work inside svg) + $(".lava-flow").tooltip + 'container': 'body' + 'placement': 'top' eruptionsBrushEnd: () => diff --git a/coffee/lavaLayer.coffee b/coffee/lavaLayer.coffee index 7fb68ab..8253322 100644 --- a/coffee/lavaLayer.coffee +++ b/coffee/lavaLayer.coffee @@ -104,10 +104,16 @@ .attr("stroke", "red") .attr("stroke-width", 10) .attr("fill", "red") - .selectAll('title') - .text((d, i) -> - "lavaflow in #{d.date.getFullYear()}" - ) + .attr("data-toggle", "tooltip") + .attr("title", (d, i) -> + "lavaflow in #{d.date.getFullYear()}" + ) + .attr("class", "lava-flow") + + # .selectAll('title') + # .text((d, i) -> + # "lavaflow in #{d.date.getFullYear()}" + # ) data: (boundingBox, flows) -> @@ -115,7 +121,7 @@ lava = mapDrawGroup.selectAll('path') .data(flows) lava.exit().remove().remove() - lava.enter().append('path').append('title') + lava.enter().append('path') #.append('title') # sets the visible map extension diff --git a/css/barchart.css b/css/barchart.css index f1a1ccb..eef86cc 100644 --- a/css/barchart.css +++ b/css/barchart.css @@ -49,3 +49,9 @@ circle { .lava-vec path { opacity: 0.2; } + +/* re-style bootstrap tooltips to be white */ +.tooltip .tooltip-inner { + background-color: #fff; + color: #000; +} diff --git a/index.html b/index.html index ee13c3b..406cbfa 100644 --- a/index.html +++ b/index.html @@ -16,6 +16,7 @@ + diff --git a/js/eruptions.js b/js/eruptions.js index fe55054..368d711 100644 --- a/js/eruptions.js +++ b/js/eruptions.js @@ -271,7 +271,11 @@ this.etna.eruptionsChart = (function() { _this.circleLayer.draw(); _this.lavaLayer.data(boundingBox, lavaFiltered); _this.lavaLayer.draw(); - return _this.map.refresh(); + _this.map.refresh(); + return $(".lava-flow").tooltip({ + 'container': 'body', + 'placement': 'top' + }); }, eruptionsBrushEnd: function() { var airport, airportShutdowns, ashFalls, crater, craterExplosions, dataFiltered, datum, destroyed, destroyedTown, earthquakeTown, earthquakes, town, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _len5, _m, _n, _ref, _ref1, _ref2, _ref3, _ref4; diff --git a/js/lavaLayer.js b/js/lavaLayer.js index 6e595c6..bf253cb 100644 --- a/js/lavaLayer.js +++ b/js/lavaLayer.js @@ -84,16 +84,16 @@ this.etna.lavaLayer = function(map, mapDrawSvg, mapDrawGroup) { default: return "M 0 0 L 0 0"; } - }).attr("stroke", "red").attr("stroke-width", 10).attr("fill", "red").selectAll('title').text(function(d, i) { + }).attr("stroke", "red").attr("stroke-width", 10).attr("fill", "red").attr("data-toggle", "tooltip").attr("title", function(d, i) { return "lavaflow in " + (d.date.getFullYear()); - }); + }).attr("class", "lava-flow"); }, data: function(boundingBox, flows) { var bounds, lava; bounds = d3.geo.bounds(boundingBox); lava = mapDrawGroup.selectAll('path').data(flows); lava.exit().remove().remove(); - return lava.enter().append('path').append('title'); + return lava.enter().append('path'); }, extent: function() { return new MM.Extent(new MM.Location(bounds[0][1], bounds[0][0]), new MM.Location(bounds[1][1], bounds[1][0])); diff --git a/js/vendor/bootstrap-tooltip.js b/js/vendor/bootstrap-tooltip.js new file mode 100755 index 0000000..835abbe --- /dev/null +++ b/js/vendor/bootstrap-tooltip.js @@ -0,0 +1,361 @@ +/* =========================================================== + * bootstrap-tooltip.js v2.3.1 + * http://twitter.github.com/bootstrap/javascript.html#tooltips + * Inspired by the original jQuery.tipsy by Jason Frame + * =========================================================== + * Copyright 2012 Twitter, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ========================================================== */ + + +!function ($) { + + "use strict"; // jshint ;_; + + + /* TOOLTIP PUBLIC CLASS DEFINITION + * =============================== */ + + var Tooltip = function (element, options) { + this.init('tooltip', element, options) + } + + Tooltip.prototype = { + + constructor: Tooltip + + , init: function (type, element, options) { + var eventIn + , eventOut + , triggers + , trigger + , i + + this.type = type + this.$element = $(element) + this.options = this.getOptions(options) + this.enabled = true + + triggers = this.options.trigger.split(' ') + + for (i = triggers.length; i--;) { + trigger = triggers[i] + if (trigger == 'click') { + this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) + } else if (trigger != 'manual') { + eventIn = trigger == 'hover' ? 'mouseenter' : 'focus' + eventOut = trigger == 'hover' ? 'mouseleave' : 'blur' + this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) + this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) + } + } + + this.options.selector ? + (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : + this.fixTitle() + } + + , getOptions: function (options) { + options = $.extend({}, $.fn[this.type].defaults, this.$element.data(), options) + + if (options.delay && typeof options.delay == 'number') { + options.delay = { + show: options.delay + , hide: options.delay + } + } + + return options + } + + , enter: function (e) { + var defaults = $.fn[this.type].defaults + , options = {} + , self + + this._options && $.each(this._options, function (key, value) { + if (defaults[key] != value) options[key] = value + }, this) + + self = $(e.currentTarget)[this.type](options).data(this.type) + + if (!self.options.delay || !self.options.delay.show) return self.show() + + clearTimeout(this.timeout) + self.hoverState = 'in' + this.timeout = setTimeout(function() { + if (self.hoverState == 'in') self.show() + }, self.options.delay.show) + } + + , leave: function (e) { + var self = $(e.currentTarget)[this.type](this._options).data(this.type) + + if (this.timeout) clearTimeout(this.timeout) + if (!self.options.delay || !self.options.delay.hide) return self.hide() + + self.hoverState = 'out' + this.timeout = setTimeout(function() { + if (self.hoverState == 'out') self.hide() + }, self.options.delay.hide) + } + + , show: function () { + var $tip + , pos + , actualWidth + , actualHeight + , placement + , tp + , e = $.Event('show') + + if (this.hasContent() && this.enabled) { + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + $tip = this.tip() + this.setContent() + + if (this.options.animation) { + $tip.addClass('fade') + } + + placement = typeof this.options.placement == 'function' ? + this.options.placement.call(this, $tip[0], this.$element[0]) : + this.options.placement + + $tip + .detach() + .css({ top: 0, left: 0, display: 'block' }) + + this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) + + pos = this.getPosition() + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + switch (placement) { + case 'bottom': + tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'top': + tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2} + break + case 'left': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth} + break + case 'right': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width} + break + } + + this.applyPlacement(tp, placement) + this.$element.trigger('shown') + } + } + + , applyPlacement: function(offset, placement){ + var $tip = this.tip() + , width = $tip[0].offsetWidth + , height = $tip[0].offsetHeight + , actualWidth + , actualHeight + , delta + , replace + + $tip + .offset(offset) + .addClass(placement) + .addClass('in') + + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + + if (placement == 'top' && actualHeight != height) { + offset.top = offset.top + height - actualHeight + replace = true + } + + if (placement == 'bottom' || placement == 'top') { + delta = 0 + + if (offset.left < 0){ + delta = offset.left * -2 + offset.left = 0 + $tip.offset(offset) + actualWidth = $tip[0].offsetWidth + actualHeight = $tip[0].offsetHeight + } + + this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') + } else { + this.replaceArrow(actualHeight - height, actualHeight, 'top') + } + + if (replace) $tip.offset(offset) + } + + , replaceArrow: function(delta, dimension, position){ + this + .arrow() + .css(position, delta ? (50 * (1 - delta / dimension) + "%") : '') + } + + , setContent: function () { + var $tip = this.tip() + , title = this.getTitle() + + $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) + $tip.removeClass('fade in top bottom left right') + } + + , hide: function () { + var that = this + , $tip = this.tip() + , e = $.Event('hide') + + this.$element.trigger(e) + if (e.isDefaultPrevented()) return + + $tip.removeClass('in') + + function removeWithAnimation() { + var timeout = setTimeout(function () { + $tip.off($.support.transition.end).detach() + }, 500) + + $tip.one($.support.transition.end, function () { + clearTimeout(timeout) + $tip.detach() + }) + } + + $.support.transition && this.$tip.hasClass('fade') ? + removeWithAnimation() : + $tip.detach() + + this.$element.trigger('hidden') + + return this + } + + , fixTitle: function () { + var $e = this.$element + if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { + $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') + } + } + + , hasContent: function () { + return this.getTitle() + } + + , getPosition: function () { + var el = this.$element[0] + return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { + width: el.offsetWidth + , height: el.offsetHeight + }, this.$element.offset()) + } + + , getTitle: function () { + var title + , $e = this.$element + , o = this.options + + title = $e.attr('data-original-title') + || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) + + return title + } + + , tip: function () { + return this.$tip = this.$tip || $(this.options.template) + } + + , arrow: function(){ + return this.$arrow = this.$arrow || this.tip().find(".tooltip-arrow") + } + + , validate: function () { + if (!this.$element[0].parentNode) { + this.hide() + this.$element = null + this.options = null + } + } + + , enable: function () { + this.enabled = true + } + + , disable: function () { + this.enabled = false + } + + , toggleEnabled: function () { + this.enabled = !this.enabled + } + + , toggle: function (e) { + var self = e ? $(e.currentTarget)[this.type](this._options).data(this.type) : this + self.tip().hasClass('in') ? self.hide() : self.show() + } + + , destroy: function () { + this.hide().$element.off('.' + this.type).removeData(this.type) + } + + } + + + /* TOOLTIP PLUGIN DEFINITION + * ========================= */ + + var old = $.fn.tooltip + + $.fn.tooltip = function ( option ) { + return this.each(function () { + var $this = $(this) + , data = $this.data('tooltip') + , options = typeof option == 'object' && option + if (!data) $this.data('tooltip', (data = new Tooltip(this, options))) + if (typeof option == 'string') data[option]() + }) + } + + $.fn.tooltip.Constructor = Tooltip + + $.fn.tooltip.defaults = { + animation: true + , placement: 'top' + , selector: false + , template: '
' + , trigger: 'hover focus' + , title: '' + , delay: 0 + , html: false + , container: false + } + + + /* TOOLTIP NO CONFLICT + * =================== */ + + $.fn.tooltip.noConflict = function () { + $.fn.tooltip = old + return this + } + +}(window.jQuery);