From 4cf5383d0d520c8eb88a961312c9fade7f9f49a1 Mon Sep 17 00:00:00 2001 From: Sebastian Lohff Date: Sat, 28 Mar 2015 19:47:52 +0100 Subject: [PATCH] Tooltips --- .gitignore | 1 + bgpdata/models.py | 4 + bgpdata/templates/bgpdata/map.html | 141 +++++++++++----- dnmapper/settings.default.py | 93 +++++++++++ static/css/tipsy.css | 25 +++ static/js/jquery.tipsy.js | 260 +++++++++++++++++++++++++++++ templates/base.html | 2 + 7 files changed, 486 insertions(+), 40 deletions(-) create mode 100644 dnmapper/settings.default.py create mode 100644 static/css/tipsy.css create mode 100644 static/js/jquery.tipsy.js diff --git a/.gitignore b/.gitignore index 53ef0cd..a537823 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .*.swp .*.swo db.sqlite3 +dnmapper/settings.py diff --git a/bgpdata/models.py b/bgpdata/models.py index a9e75a1..2804a56 100644 --- a/bgpdata/models.py +++ b/bgpdata/models.py @@ -84,6 +84,10 @@ class AS(models.Model): def getPeerings(self): return Peering.objects.filter(Q(as1=self)|Q(as2=self)) + def formatLastSeen(self): + if self.lastSeen: + return self.lastSeen.startTime.strftime("%d.%m.%Y %H:%I") + class BorderRouter(models.Model): # as id, ip, check method, pingable, reachable # unique: (crawl_id, asno, as id) diff --git a/bgpdata/templates/bgpdata/map.html b/bgpdata/templates/bgpdata/map.html index 2e15e5d..374f250 100644 --- a/bgpdata/templates/bgpdata/map.html +++ b/bgpdata/templates/bgpdata/map.html @@ -25,7 +25,7 @@ var asdata = [ {% for AS in ASses %} - {id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.lastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}}{%if not forloop.last%},{%endif%} + {id: {{AS.pk}}, nodetype: "AS", asnumber: {{AS.number}}, label: "{{AS.number}}", neighbors: {{AS.getPeerings.count}}, crawled: {%if AS.directlyCrawled%}true{%else%}false{%endif%}, online: {%if AS.online%}true{%else%}false{%endif%}, lastSeenDate: "{{AS.formatLastSeen}}", lastSeen: {%if AS.lastSeen%}{{AS.lastSeen.pk}}{%else%}null{%endif%}, dismiss: true}{%if not forloop.last%},{%endif%} {%endfor%} ]; @@ -63,7 +63,6 @@ var svg = d3.select('#plotwin') .attr('width', width) .attr('height', height); -console.log(asdata); var force = d3.layout.force() .nodes(asdata) .links(peerings) @@ -72,9 +71,7 @@ var force = d3.layout.force() // .linkDistance(70) .linkStrength(0.65) .linkDistance(function(l) { - console.log(l); neighs = Math.min(l.source.neighbors, l.target.neighbors); - console.log(neighs, "neighbors"); switch(neighs) { case 0: return 40; case 1: @@ -103,6 +100,7 @@ var node = svg.selectAll('.node') .data(asdata) .enter() .append("g") + .attr("id", function(d) { return "node-" + d.id; }) .call(force.drag); node.append("ellipse") @@ -120,6 +118,7 @@ node.append("ellipse") .attr("stroke", "black") .attr("stroke-width", "1px"); + node.append('text') .attr("font-family", "sans-serif") .attr("font-size", "13px") @@ -128,6 +127,57 @@ node.append('text') .attr("text-anchor", "middle") .text(function(d) { return d.label; }); +var lastMouseNode = null; +node.on("mouseover", function(d) { + if(lastMouseNode) { + if(!d.dismiss && lastMouseNode.id == d.id) + return; + else + $("#node-"+lastMouseNode.id).tipsy("hide"); + } + + d.dismiss = true; + $("#node-"+d.id).tipsy("show"); + + lastMouseNode = d; + }) + .on("mouseout", function(d) { + if(d.dismiss) + $("#node-"+d.id).tipsy("hide"); + }); + +$('svg g').tipsy({ + gravity: 'e', + offset: 20, + html: true, + trigger: 'manual', + title: function() { + var d = this.__data__; + var content = ''; + if(d.nodetype == 'AS') { + var state = null; + if(d.online) + state = 'Online'; + else if(d.lastSeen) + state = 'Offline'; + else + state = 'Never seen'; + + content = 'AS '+d.asnumber+'
'; + content += ''; + content += ''; + if(d.lastSeen) { + content += ''; + } + if(d.crawled) { + content += ''; + } + content += '
State'+state+'
Last seenCrawl '+d.lastSeen+'
'+d.lastSeenDate+'
NoteDirectly crawled
'; + } + return content; + } +}); + force.on("tick", function() { //node.attr('cx', function(d) { return d.x; }) // .attr('cy', function(d) { return d.y; }); @@ -139,51 +189,62 @@ force.on("tick", function() { .attr("y2", function(d) { return d.target.y; }); }); + var lastNode = null; function click(d) { if(d3.event.defaultPrevented) return; - if(lastNode) - console.log("last time you clicked on another node", d); + + //if(lastNode) + // $("#node-" + lastNode.id).tipsy("hide"); + d.dismiss = false; + //$("#node-" + d.id).tipsy("show"); + + $("#infowin").fadeOut('fast', function() { // set progress bar $("#infowin").html('
Loading...
'); - console.log("fading done"); var content = ''; - if(d.nodetype == 'AS' && d.crawled) { - console.log("doing javascript..."); - $.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) { - console.log("SUCCESS", result); - $("#infowin").html(''); - $("#infowin").fadeIn('fast', function() {}); - for(var i=0; i'+ann.nextHop+''+ann.ASPath+''; - } - astable += ''; - console.log("Sending stuff for", currRouter.id, "aka", currRouter.routerID); - $('#infowin').append(astable); - }}); - })(result.objects[i]); - } + if(d.nodetype == 'AS') { + //$("#node-"+d.id).popover({content: "Hallo Katze! :)"}); + //$("#node-"+d.id).popover('show'); + //console.log("My element", $("#node-"+d.id)); + //$("#node-"+d.id).tipsy({ + // gravity: 'e', + // html: true, + // //trigger: 'focus', + // trigger: 'hover', + // title: function() { + // var d = this.__data__; + // astable = '
FOOObar
'; + // return astable; + // } + //}); - }}); - astable = ''; - astable += ''; - astable += ''; - astable += ''; - astable += ''; - - astable += '
AS'+ d.asnumber +'
Directly crawled
Online
'; + if(d.crawled) { + $.ajax({url: "/map/api/borderrouter/?AS__crawl={{crawl.pk}}&AS__number=" + d.asnumber, success: function(result) { + $("#infowin").html(''); + $("#infowin").fadeIn('fast', function() {}); + for(var i=0; i'; + astable += 'NetworkNext HopAS Path'; + for(var j=0; j'+ann.nextHop+''+ann.ASPath+''; + } + astable += ''; + $('#infowin').append(astable); + }}); + })(result.objects[i]); + } + + }}); + } } else { @@ -191,10 +252,10 @@ function click(d) { $("#infowin").fadeIn('fast', function() {}); } }); - console.log("Single click on", d); lastNode = d; } -node.on("click", click) +node.on("click", click); + force.start(); diff --git a/dnmapper/settings.default.py b/dnmapper/settings.default.py new file mode 100644 index 0000000..fa8ef5c --- /dev/null +++ b/dnmapper/settings.default.py @@ -0,0 +1,93 @@ +""" +Django settings for dnmapper project. + +For more information on this file, see +https://docs.djangoproject.com/en/1.7/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.7/ref/settings/ +""" + +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +import os +BASE_DIR = os.path.dirname(os.path.dirname(__file__)) + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = 'crv*hfx2pkxvq1s!)dbz*hdu+r7u2$y4djf6_#6mm)shk9e!58' + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +TEMPLATE_DEBUG = True + +ALLOWED_HOSTS = [] + +STATICFILES_DIRS = ( + 'static/', +) + +# Application definition + +INSTALLED_APPS = ( + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'bgpdata', + 'tastypie', +) + +API_LIMIT_PER_PAGE = 100 + +MIDDLEWARE_CLASSES = ( + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +) + +ROOT_URLCONF = 'dnmapper.urls' + +WSGI_APPLICATION = 'dnmapper.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.7/ref/settings/#databases + +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +# Internationalization +# https://docs.djangoproject.com/en/1.7/topics/i18n/ + +LANGUAGE_CODE = 'de-de' + +TIME_ZONE = 'CET' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True +TEMPLATE_DIRS = ( + 'templates/', +) + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.7/howto/static-files/ + +STATIC_URL = '/static/' diff --git a/static/css/tipsy.css b/static/css/tipsy.css new file mode 100644 index 0000000..a50b701 --- /dev/null +++ b/static/css/tipsy.css @@ -0,0 +1,25 @@ +.tipsy { font-size: 14px; position: absolute; padding: 5px; z-index: 100000; } + .tipsy-inner { background-color: #000; color: #FFF; max-width: 200px; padding: 5px 8px 4px 8px; text-align: center; } + + /* Rounded corners */ + .tipsy-inner { border-radius: 3px; -moz-border-radius: 3px; -webkit-border-radius: 3px; } + + /* Uncomment for shadow */ + /*.tipsy-inner { box-shadow: 0 0 5px #000000; -webkit-box-shadow: 0 0 5px #000000; -moz-box-shadow: 0 0 5px #000000; }*/ + + .tipsy-arrow { position: absolute; width: 0; height: 0; line-height: 0; border: 5px dashed #000; } + + /* Rules to colour arrows */ + .tipsy-arrow-n { border-bottom-color: #000; } + .tipsy-arrow-s { border-top-color: #000; } + .tipsy-arrow-e { border-left-color: #000; } + .tipsy-arrow-w { border-right-color: #000; } + + .tipsy-n .tipsy-arrow { top: 0px; left: 50%; margin-left: -5px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-nw .tipsy-arrow { top: 0; left: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-ne .tipsy-arrow { top: 0; right: 10px; border-bottom-style: solid; border-top: none; border-left-color: transparent; border-right-color: transparent;} + .tipsy-s .tipsy-arrow { bottom: 0; left: 50%; margin-left: -5px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-sw .tipsy-arrow { bottom: 0; left: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-se .tipsy-arrow { bottom: 0; right: 10px; border-top-style: solid; border-bottom: none; border-left-color: transparent; border-right-color: transparent; } + .tipsy-e .tipsy-arrow { right: 0; top: 50%; margin-top: -5px; border-left-style: solid; border-right: none; border-top-color: transparent; border-bottom-color: transparent; } + .tipsy-w .tipsy-arrow { left: 0; top: 50%; margin-top: -5px; border-right-style: solid; border-left: none; border-top-color: transparent; border-bottom-color: transparent; } diff --git a/static/js/jquery.tipsy.js b/static/js/jquery.tipsy.js new file mode 100644 index 0000000..b0a807e --- /dev/null +++ b/static/js/jquery.tipsy.js @@ -0,0 +1,260 @@ +// tipsy, facebook style tooltips for jquery +// version 1.0.0a +// (c) 2008-2010 jason frame [jason@onehackoranother.com] +// released under the MIT license + +(function($) { + + function maybeCall(thing, ctx) { + return (typeof thing == 'function') ? (thing.call(ctx)) : thing; + }; + + function isElementInDOM(ele) { + while (ele = ele.parentNode) { + if (ele == document) return true; + } + return false; + }; + + function Tipsy(element, options) { + this.$element = $(element); + this.options = options; + this.enabled = true; + this.fixTitle(); + }; + + Tipsy.prototype = { + show: function() { + var title = this.getTitle(); + if (title && this.enabled) { + var $tip = this.tip(); + + $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); + $tip[0].className = 'tipsy'; // reset classname in case of dynamic gravity + $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); + + var pos = $.extend({}, this.$element.offset(), { + width: this.$element[0].offsetWidth, + height: this.$element[0].offsetHeight + }); + + var actualWidth = $tip[0].offsetWidth, + actualHeight = $tip[0].offsetHeight, + gravity = maybeCall(this.options.gravity, this.$element[0]); + + var tp; + switch (gravity.charAt(0)) { + case 'n': + tp = {top: pos.top + pos.height + this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 's': + tp = {top: pos.top - actualHeight - this.options.offset, left: pos.left + pos.width / 2 - actualWidth / 2}; + break; + case 'e': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth - this.options.offset}; + // XXX HACK: i want to set another offset so apparently I have to hardcode this into the code. obviously. + tp = {top: pos.top + pos.height / 2 - actualHeight / 2 + this.options.offset, left: pos.left - actualWidth}; + break; + case 'w': + tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width + this.options.offset}; + break; + } + + if (gravity.length == 2) { + if (gravity.charAt(1) == 'w') { + tp.left = pos.left + pos.width / 2 - 15; + } else { + tp.left = pos.left + pos.width / 2 - actualWidth + 15; + } + } + + $tip.css(tp).addClass('tipsy-' + gravity); + $tip.find('.tipsy-arrow')[0].className = 'tipsy-arrow tipsy-arrow-' + gravity.charAt(0); + if (this.options.className) { + $tip.addClass(maybeCall(this.options.className, this.$element[0])); + } + + if (this.options.fade) { + $tip.stop().css({opacity: 0, display: 'block', visibility: 'visible'}).animate({opacity: this.options.opacity}); + } else { + $tip.css({visibility: 'visible', opacity: this.options.opacity}); + } + } + }, + + hide: function() { + if (this.options.fade) { + this.tip().stop().fadeOut(function() { $(this).remove(); }); + } else { + this.tip().remove(); + } + }, + + fixTitle: function() { + var $e = this.$element; + if ($e.attr('title') || typeof($e.attr('original-title')) != 'string') { + $e.attr('original-title', $e.attr('title') || '').removeAttr('title'); + } + }, + + getTitle: function() { + var title, $e = this.$element, o = this.options; + this.fixTitle(); + var title, o = this.options; + if (typeof o.title == 'string') { + title = $e.attr(o.title == 'title' ? 'original-title' : o.title); + } else if (typeof o.title == 'function') { + title = o.title.call($e[0]); + } + title = ('' + title).replace(/(^\s*|\s*$)/, ""); + return title || o.fallback; + }, + + tip: function() { + if (!this.$tip) { + this.$tip = $('
').html('
'); + this.$tip.data('tipsy-pointee', this.$element[0]); + } + return this.$tip; + }, + + 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; } + }; + + $.fn.tipsy = function(options) { + + if (options === true) { + return this.data('tipsy'); + } else if (typeof options == 'string') { + var tipsy = this.data('tipsy'); + if (tipsy) tipsy[options](); + return this; + } + + options = $.extend({}, $.fn.tipsy.defaults, options); + + function get(ele) { + var tipsy = $.data(ele, 'tipsy'); + if (!tipsy) { + tipsy = new Tipsy(ele, $.fn.tipsy.elementOptions(ele, options)); + $.data(ele, 'tipsy', tipsy); + } + return tipsy; + } + + function enter() { + var tipsy = get(this); + tipsy.hoverState = 'in'; + if (options.delayIn == 0) { + tipsy.show(); + } else { + tipsy.fixTitle(); + setTimeout(function() { if (tipsy.hoverState == 'in') tipsy.show(); }, options.delayIn); + } + }; + + function leave() { + var tipsy = get(this); + tipsy.hoverState = 'out'; + if (options.delayOut == 0) { + tipsy.hide(); + } else { + setTimeout(function() { if (tipsy.hoverState == 'out') tipsy.hide(); }, options.delayOut); + } + }; + + if (!options.live) this.each(function() { get(this); }); + + if (options.trigger != 'manual') { + var binder = options.live ? 'live' : 'bind', + eventIn = options.trigger == 'hover' ? 'mouseenter' : 'focus', + eventOut = options.trigger == 'hover' ? 'mouseleave' : 'blur'; + this[binder](eventIn, enter)[binder](eventOut, leave); + } + + return this; + + }; + + $.fn.tipsy.defaults = { + className: null, + delayIn: 0, + delayOut: 0, + fade: false, + fallback: '', + gravity: 'n', + html: false, + live: false, + offset: 0, + opacity: 0.8, + title: 'title', + trigger: 'hover' + }; + + $.fn.tipsy.revalidate = function() { + $('.tipsy').each(function() { + var pointee = $.data(this, 'tipsy-pointee'); + if (!pointee || !isElementInDOM(pointee)) { + $(this).remove(); + } + }); + }; + + // Overwrite this method to provide options on a per-element basis. + // For example, you could store the gravity in a 'tipsy-gravity' attribute: + // return $.extend({}, options, {gravity: $(ele).attr('tipsy-gravity') || 'n' }); + // (remember - do not modify 'options' in place!) + $.fn.tipsy.elementOptions = function(ele, options) { + return $.metadata ? $.extend({}, options, $(ele).metadata()) : options; + }; + + $.fn.tipsy.autoNS = function() { + return $(this).offset().top > ($(document).scrollTop() + $(window).height() / 2) ? 's' : 'n'; + }; + + $.fn.tipsy.autoWE = function() { + return $(this).offset().left > ($(document).scrollLeft() + $(window).width() / 2) ? 'e' : 'w'; + }; + + /** + * yields a closure of the supplied parameters, producing a function that takes + * no arguments and is suitable for use as an autogravity function like so: + * + * @param margin (int) - distance from the viewable region edge that an + * element should be before setting its tooltip's gravity to be away + * from that edge. + * @param prefer (string, e.g. 'n', 'sw', 'w') - the direction to prefer + * if there are no viewable region edges effecting the tooltip's + * gravity. It will try to vary from this minimally, for example, + * if 'sw' is preferred and an element is near the right viewable + * region edge, but not the top edge, it will set the gravity for + * that element's tooltip to be 'se', preserving the southern + * component. + */ + $.fn.tipsy.autoBounds = function(margin, prefer) { + return function() { + var dir = {ns: prefer[0], ew: (prefer.length > 1 ? prefer[1] : false)}, + boundTop = $(document).scrollTop() + margin, + boundLeft = $(document).scrollLeft() + margin, + $this = $(this); + + if ($this.offset().top < boundTop) dir.ns = 'n'; + if ($this.offset().left < boundLeft) dir.ew = 'w'; + if ($(window).width() + $(document).scrollLeft() - $this.offset().left < margin) dir.ew = 'e'; + if ($(window).height() + $(document).scrollTop() - $this.offset().top < margin) dir.ns = 's'; + + return dir.ns + (dir.ew ? dir.ew : ''); + } + }; + +})(jQuery); diff --git a/templates/base.html b/templates/base.html index d1b3345..e54b6f9 100644 --- a/templates/base.html +++ b/templates/base.html @@ -4,10 +4,12 @@ + {% block head %}{% endblock %} +