tweetcast/nodejs-bis/client/js/script.js
changeset 360 d49991fe4892
child 374 0c4acfa2aea1
equal deleted inserted replaced
359:863871f4c44c 360:d49991fe4892
       
     1 /**
       
     2  * @author raph
       
     3  */
       
     4 
       
     5 var socket,
       
     6     tlPaper,
       
     7     twCx = {
       
     8         zoomLevel : 1,
       
     9         followLast : true,
       
    10         position : 0,
       
    11         date_levels : [
       
    12             15 * 60 * 1000,
       
    13             5 * 60 * 1000,
       
    14             60 * 1000,
       
    15             15 * 1000
       
    16         ],
       
    17         timeLevel : 2,
       
    18         deltaX : 30,
       
    19         tlWidth : 98,
       
    20         tlHeight : 450,
       
    21         globalWords : {}
       
    22         },
       
    23     tlBuffer = '',
       
    24     relHover = null,
       
    25     wheelDelta = 0,
       
    26     rx_word = /[^ \.&;,'"!\?@#\d\(\)\+\[\]\\\…\-«»:\/]{3,}/g;
       
    27 
       
    28 function arc(source, target) {
       
    29     var x3 = .3 * target.y - .3 * source.y + .8 * source.x + .2 * target.x;
       
    30     var y3 = .8 * source.y + .2 * target.y - .3 * target.x + .3 * source.x;
       
    31     var x4 = .3 * target.y - .3 * source.y + .2 * source.x + .8 * target.x;
       
    32     var y4 = .2 * source.y + .8 * target.y - .3 * target.x + .3 * source.x;
       
    33     return "M" + source.x + " " + source.y + "C" + [x3, y3, x4, y4, target.x, target.y].join(" ");
       
    34 }
       
    35 
       
    36 function countWords(text, wordobj) {
       
    37     var tab = text.match(rx_word);
       
    38     for (var i in tab) {
       
    39         var word = tab[i].toLowerCase();
       
    40         if (wordobj[word]) {
       
    41             wordobj[word]++;
       
    42         } else {
       
    43             wordobj[word] = 1;
       
    44         }
       
    45     }
       
    46 }
       
    47 
       
    48 function addTweet(tweet) {
       
    49     function backRef(source_id, target_id, type) {
       
    50         var target = tweetById(target_id);
       
    51         if (target) {
       
    52             var brobj = {
       
    53                 "referenced_by_id" : source_id,
       
    54                 "type" : type
       
    55             }
       
    56             if (target.backRefs) {
       
    57                 target.backRefs.push(brobj);
       
    58             } else {
       
    59                 target.backRefs = [ brobj ]
       
    60             }
       
    61         }
       
    62     }
       
    63     
       
    64     var tab = tweet.text.split(/\&\#|\;/),
       
    65         txt = '';
       
    66     for (i = 0; i < tab.length; i++) {
       
    67         txt += (i % 2 && parseInt(tab[i]) != NaN) ? String.fromCharCode(tab[i]) : tab[i];
       
    68     }
       
    69     tweet.text = txt;
       
    70     
       
    71     twCx.tweets.push(tweet);
       
    72     twCx.idIndex.push(tweet.id);
       
    73 
       
    74     if (tweet.in_reply_to_status_id) {
       
    75         backRef( tweet.id, tweet.in_reply_to_status_id, "reply" );
       
    76     }
       
    77     if (tweet.retweeted_status) {
       
    78         backRef( tweet.id,  tweet.retweeted_status.id, "retweet" );
       
    79     }
       
    80     
       
    81     countWords(tweet.text, twCx.globalWords);
       
    82     
       
    83     var creadate = new Date(tweet.created_at).valueOf();
       
    84     if (!twCx.timeline.length) {
       
    85         twCx.timeline = [ populateDateStruct(0, twCx.date_levels[0] * parseInt(creadate / twCx.date_levels[0])) ]
       
    86     }
       
    87     while (creadate > twCx.timeline[twCx.timeline.length - 1].end) {
       
    88         twCx.timeline.push( populateDateStruct(0, twCx.timeline[twCx.timeline.length - 1].end) );
       
    89     }
       
    90     insertIntoDateStruct(twCx.timeline, tweet);
       
    91 }
       
    92 
       
    93 function getSliceContent(slice) {
       
    94     if (slice.slices) {
       
    95         var result = [];
       
    96         for (var i in slice.slices) {
       
    97             result = result.concat(getSliceContent(slice.slices[i]));
       
    98         }
       
    99     } else {
       
   100         var result = slice.tweets;
       
   101     }
       
   102     return result;
       
   103 }
       
   104 
       
   105 function flattenDateStruct(slices, target_level) {
       
   106     var current_level = slices[0].level,
       
   107         result = [];
       
   108     if (current_level < target_level) {
       
   109         if (slices[0].slices) {
       
   110             for (var i in slices) {
       
   111                 result = result.concat(flattenDateStruct(slices[i].slices, target_level));
       
   112             }
       
   113         }
       
   114     }
       
   115     else {
       
   116         for (var i in slices) {
       
   117             result.push({
       
   118                 "start" : slices[i].start,
       
   119                 "end" : slices[i].end,
       
   120                 "tweets" : getSliceContent(slices[i])
       
   121             });
       
   122         }
       
   123     }
       
   124     return result;
       
   125 }
       
   126 
       
   127 function trimFDS(slices) {
       
   128     while (slices[0].tweets.length == 0) {
       
   129         slices.splice(0,1);
       
   130     }
       
   131     while (slices[slices.length - 1].tweets.length == 0) {
       
   132         slices.pop();
       
   133     }
       
   134     return slices;
       
   135 }
       
   136 
       
   137 function populateDateStruct(level, start) {
       
   138     var end = start + twCx.date_levels[level],
       
   139         struct = {
       
   140             "level" : level,
       
   141             "start" : start,
       
   142             "end" : end
       
   143         };
       
   144     if (level < twCx.date_levels.length - 1) {
       
   145         struct.slices = [];
       
   146         var newstart = start;
       
   147         while (newstart < end) {
       
   148             struct.slices.push(populateDateStruct(level + 1, newstart));
       
   149             newstart += twCx.date_levels[level + 1];
       
   150         }
       
   151     } else {
       
   152         struct.tweets = [];
       
   153     }
       
   154     return struct;
       
   155 }
       
   156 
       
   157 function insertIntoDateStruct(slices, tweet) {
       
   158     var creadate = new Date(tweet.created_at).valueOf();
       
   159     for (var i in slices) {
       
   160         if (creadate < slices[i].end) {
       
   161             if (slices[i].slices) {
       
   162                 insertIntoDateStruct(slices[i].slices, tweet);
       
   163             } else {
       
   164                 slices[i].tweets.push(tweet.id);
       
   165             }
       
   166             break;
       
   167         }
       
   168     }
       
   169 }
       
   170 
       
   171 function placeHolder(className) {
       
   172     return '<li class="placeholder ' + className + '"></li>';
       
   173 }
       
   174 
       
   175 function tweetById(tweetid) {
       
   176     var pos = twCx.idIndex.indexOf(tweetid);
       
   177     return (pos == -1) ? false : twCx.tweets[pos];
       
   178 }
       
   179 
       
   180 function selectTweet(tweetid) {
       
   181     var pos = twCx.idIndex.indexOf(tweetid);
       
   182     if (pos != -1) {
       
   183         twCx.position = pos;
       
   184         twCx.followLast = (twCx.position == twCx.tweets.length - 1);
       
   185         updateDisplay()
       
   186     }
       
   187 }
       
   188 
       
   189 function tweetToHtml(tweet, className, elName) {
       
   190     if (!tweet) {
       
   191         return placeHolder(className);
       
   192     }
       
   193     var el = (elName ? elName : 'li');
       
   194     var html = '<' + el + ' class="tweet ' + className + '" id="tweet_' + tweet.id + '"';
       
   195     if (className != 'full') {
       
   196         html += ' onclick="selectTweet(\'' + tweet.id + '\'); return false;" onmouseover="rolloverTweet(\'' + tweet.id + '\');"';
       
   197     } else {
       
   198         html += ' onmouseover="$(\'#hovertweet\').hide();"';
       
   199     }
       
   200     if (twCx.followLast && className == 'full' && el == 'li') {
       
   201         html += ' style="display: none"';
       
   202     }
       
   203     html += '>';
       
   204     if (tweet.annotations.length) {
       
   205         html += '<div class="annotations">';
       
   206         for (var i in tweet.annotations) {
       
   207             html += '<div class="annotation" style="width:' + (100/tweet.annotations.length) + '%; background:' + annotations[tweet.annotations[i]].colors[(className == 'icons' ? 'timeline' : 'tweet')] + '"></div>';
       
   208         }
       
   209         html += '</div>';
       
   210     }
       
   211     html += '<div class="twmain">';
       
   212     a_user = '<a href="http://twitter.com/' + tweet.user.screen_name + '" var target="_blank" title="' + tweet.user.name + '">';
       
   213     html += '<div class="around_img">' + a_user + '<img class="profile_image" src="' + tweet.user.profile_image_url + '" /></a>';
       
   214     if (className == 'full') {
       
   215         html += '<p class="created_at">' + new Date(tweet.created_at).toLocaleTimeString() + '</p>';
       
   216     }
       
   217     html += '</div>';
       
   218     if (className != 'icons') {
       
   219         lastend = 0;
       
   220         var txt = '',
       
   221             entities = [];
       
   222         for (var i in tweet.entities.hashtags) {
       
   223             entities.push({
       
   224                 "start" : tweet.entities.hashtags[i].indices[0],
       
   225                 "end" : tweet.entities.hashtags[i].indices[1],
       
   226                 "html" : '<a href="http://twitter.com/search?q=%23' + tweet.entities.hashtags[i].text + '" target="_blank">#' + tweet.entities.hashtags[i].text + '</a>'
       
   227             });
       
   228         }
       
   229         for (var i in tweet.entities.urls) {
       
   230             entities.push({
       
   231                 "start" : tweet.entities.urls[i].indices[0],
       
   232                 "end" : tweet.entities.urls[i].indices[1],
       
   233                 "html" : '<a href="' + tweet.entities.urls[i].expanded_url + '" target="_blank">' + tweet.entities.urls[i].expanded_url + '</a>'
       
   234             });
       
   235         }
       
   236         for (var i in tweet.entities.user_mentions) {
       
   237             entities.push({
       
   238                 "start" : tweet.entities.user_mentions[i].indices[0],
       
   239                 "end" : tweet.entities.user_mentions[i].indices[1],
       
   240                 "html" : '<a href="http://twitter.com/' + tweet.entities.user_mentions[i].screen_name + '" target="_blank" title="' + tweet.entities.user_mentions[i].name + '">@' + tweet.entities.user_mentions[i].screen_name + '</a>'
       
   241             });
       
   242         }
       
   243         entities.sort(function(a, b) { return a.start - b.start });
       
   244         for (var i in entities) {
       
   245             txt += tweet.text.substring(lastend, entities[i].start) + entities[i].html;
       
   246             lastend = entities[i].end;
       
   247         }
       
   248         txt += tweet.text.substring(lastend);
       
   249         html += '<p class="tweet_text"><b>' + a_user + '@' + tweet.user.screen_name + '</b></a>: ' + txt + '</p>';
       
   250     }
       
   251     html += '</div></' + el + '>';
       
   252     return html;
       
   253 }
       
   254 
       
   255 function tlIdFromPos(x, y) {
       
   256     if (x < twCx.deltaX) {
       
   257         return null;
       
   258     }
       
   259     var ligne = Math.floor(( twCx.tlHeight - y ) / twCx.scaleY),
       
   260         colonne = Math.floor(( x - twCx.deltaX ) / twCx.scaleX ),
       
   261         l = 0;
       
   262     if (colonne >= twCx.tlOnDisplay[ligne].totalTweets) {
       
   263         return null;
       
   264     }
       
   265     for (var i in twCx.tlOnDisplay[ligne].displayData) {
       
   266         var nl = l + twCx.tlOnDisplay[ligne].displayData[i].length;
       
   267         if (colonne < nl) {
       
   268             return {
       
   269                 "id" : twCx.tlOnDisplay[ligne].displayData[i][colonne - l],
       
   270                 "annotation" : i
       
   271             }
       
   272         }
       
   273         l = nl;
       
   274     }
       
   275 }
       
   276 
       
   277 function tlPosTweet(tweet, annotation) {
       
   278     var x,
       
   279         y,
       
   280         dt = new Date(tweet.created_at).valueOf(),
       
   281         ann = ( annotation ? annotation : ( tweet.annotations.length ? tweet.annotations[0] : 'default' ) );
       
   282     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   283         if (twCx.tlOnDisplay[i].end > dt) {
       
   284             y = twCx.tlHeight - (i + .5) * twCx.scaleY;
       
   285             var l = 0;
       
   286             for (var j in twCx.tlOnDisplay[i].displayData) {
       
   287                 if (j == ann) {
       
   288                     var p = twCx.tlOnDisplay[i].displayData[j].indexOf(tweet.id);
       
   289                     if (p != -1) {
       
   290                         x = twCx.deltaX + twCx.scaleX * ( p + l + .5 );
       
   291                     }
       
   292                     break;
       
   293                 }
       
   294                 l += twCx.tlOnDisplay[i].displayData[j].length;
       
   295             }
       
   296             break;
       
   297         }
       
   298     }
       
   299     return ( x && y ? { "x" : x, "y" : y } : null);
       
   300 }
       
   301 
       
   302 function rolloverTweet(tweetid, annotation) {
       
   303     var t = tweetById(tweetid),
       
   304         p = tlPosTweet(t, annotation);
       
   305     if (t && p) {
       
   306         var ptl = $("#timeline").offset();
       
   307         $("#hovercontent").html(tweetToHtml(t, 'full', 'div'));
       
   308         $("#hovertweet").css({
       
   309             "left" : parseInt(ptl.left + p.x) + "px",
       
   310             "top" : parseInt(ptl.top + p.y),
       
   311             "display" : "block"});
       
   312         if (relHover) {
       
   313             relHover.remove();
       
   314         }
       
   315         relHover = drawTweetPos(p, '#ffffff')
       
   316     }
       
   317 }
       
   318 
       
   319 function drawTweetPos(pos, color) {
       
   320     var rel = tlPaper.rect(pos.x - .5 * twCx.scaleX, pos.y - .5 * twCx.scaleY, twCx.scaleX, twCx.scaleY);
       
   321     rel.attr({ "stroke" : color });
       
   322     return rel;
       
   323 }
       
   324 
       
   325 function updateDisplay() {
       
   326     var p = twCx.position,
       
   327         l = twCx.tweets.length,
       
   328         lines = 0,
       
   329         html = '',
       
   330         tweetsOnDisplay = [],
       
   331         localWords = {};
       
   332         
       
   333     function pushTweet(tp, className) {
       
   334         if (tp < l && tp >= 0) {
       
   335             html += tweetToHtml(twCx.tweets[tp], className)
       
   336             tweetsOnDisplay.push(tp);
       
   337             countWords(twCx.tweets[tp].text, localWords);
       
   338         } else {
       
   339             html += placeHolder(className);
       
   340         }
       
   341     }
       
   342     
       
   343     if (l > p + 18) {
       
   344         lines++;
       
   345         for (var i = p + 31; i >= p + 18; i--) {
       
   346             pushTweet(i, 'icons');
       
   347         }
       
   348     }
       
   349     if (l > p + 4) {
       
   350         lines++;
       
   351         for (var i = p + 17; i >= p + 4; i--) {
       
   352             pushTweet(i, 'icons');
       
   353         }
       
   354     }
       
   355     for (var k = 3; k >= 1; k--) {
       
   356         if (l > p + k) {
       
   357             lines++;
       
   358             pushTweet(p + k, 'half');
       
   359         }
       
   360     }
       
   361     pushTweet(p, 'full');
       
   362     var n = p - 1;
       
   363     for (var i = 0; i < Math.min(6, Math.max(3, 6 - lines)); i++) {
       
   364         if (n < 0) {
       
   365             break;
       
   366         }
       
   367         pushTweet(n, 'half');
       
   368         n--;
       
   369     }
       
   370     for (var i = 0; i < 14 * Math.min(4, Math.max(2, 7 - lines)); i++) {
       
   371         if (n < 0) {
       
   372             break;
       
   373         }
       
   374         pushTweet(n, 'icons');
       
   375         n--;
       
   376     }
       
   377     if (html != tlBuffer) {
       
   378         $("#tweetlist").html(html);
       
   379         $(".tweet.full").fadeIn();
       
   380         tlBuffer = html;
       
   381     }
       
   382     
       
   383     for (var j in localWords) {
       
   384         if (localWords[j] < 2) delete localWords[j];
       
   385     }
       
   386     var tab = [];
       
   387     for (var j in localWords) {
       
   388         tab.push({
       
   389             "word": j,
       
   390             "freq" : localWords[j] / Math.log(twCx.globalWords[j])
       
   391         });
       
   392     }
       
   393     tab.sort( function(a,b){ return b.freq - a.freq }).splice(10);
       
   394     $("#motscles").html(tab.map(function(t) { return t.word }).join(", "))
       
   395     
       
   396     twCx.tlOnDisplay = trimFDS(flattenDateStruct(twCx.timeline, twCx.timeLevel));
       
   397     twCx.scaleY = twCx.tlHeight / twCx.tlOnDisplay.length;
       
   398     var maxTweets = 0,
       
   399         startTl = 0,
       
   400         endTl = 0,
       
   401         startTw = new Date(twCx.tweets[tweetsOnDisplay[tweetsOnDisplay.length - 1]].created_at).valueOf(),
       
   402         endTw = new Date(twCx.tweets[tweetsOnDisplay[0]].created_at).valueOf();
       
   403     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   404         if (startTw >= twCx.tlOnDisplay[i].start && startTw < twCx.tlOnDisplay[i].end) {
       
   405             startTl = i;
       
   406         }
       
   407         if (endTw >= twCx.tlOnDisplay[i].start && endTw < twCx.tlOnDisplay[i].end) {
       
   408             endTl = i;
       
   409         }
       
   410         var displayData = {};
       
   411         for (var j in annotations) {
       
   412             displayData[j] = [];
       
   413         }
       
   414         for (var j in twCx.tlOnDisplay[i].tweets) {
       
   415             var tweetid = twCx.tlOnDisplay[i].tweets[j],
       
   416                 tweet = tweetById(tweetid);
       
   417             if (tweet) {
       
   418                 if (tweet.annotations.length) {
       
   419                     for (var k in tweet.annotations) {
       
   420                         displayData[tweet.annotations[k]].push(tweetid);
       
   421                     }
       
   422                 } else {
       
   423                     displayData['default'].push(tweetid);
       
   424                 }
       
   425             }
       
   426         }
       
   427         var nbT = 0;
       
   428         for (var j in displayData) {
       
   429             nbT += displayData[j].length;
       
   430         }
       
   431         maxTweets = Math.max(maxTweets, nbT);
       
   432         twCx.tlOnDisplay[i].displayData = displayData;
       
   433         twCx.tlOnDisplay[i].totalTweets = nbT;
       
   434     }
       
   435     twCx.scaleX = ( twCx.tlWidth - twCx.deltaX ) / maxTweets;
       
   436     tlPaper.clear();
       
   437     relHover = null;
       
   438     
       
   439     // dessin de la date de début
       
   440     
       
   441     tlPaper.text(2, twCx.tlHeight - 7, new Date(twCx.tlOnDisplay[0].start).toTimeString().substr(0,5))
       
   442         .attr({ "text-anchor" : "start", "font-size": "9px" });
       
   443     
       
   444     // dessin de la date de fin
       
   445     
       
   446     tlPaper.text(2, 7, new Date(twCx.tlOnDisplay[twCx.tlOnDisplay.length - 1].end).toTimeString().substr(0,5))
       
   447         .attr({ "text-anchor" : "start", "font-size": "9px" });
       
   448     
       
   449     for (var i = 0; i < twCx.tlOnDisplay.length; i++) {
       
   450         var n = 0,
       
   451             posY = twCx.tlHeight - ( i + 1 ) * twCx.scaleY;
       
   452         for (var j in twCx.tlOnDisplay[i].displayData) {
       
   453             var l = twCx.tlOnDisplay[i].displayData[j].length;
       
   454             if (l > 0) {
       
   455                 tlPaper.rect( twCx.deltaX + n * twCx.scaleX, posY, l * twCx.scaleX, twCx.scaleY )
       
   456                     .attr({"stroke" : "none", "fill" : annotations[j].colors.timeline });
       
   457                 n += l;
       
   458             }
       
   459         }
       
   460         
       
   461         // Si on est à une demi-heure, on trace un axe secondaire + heure
       
   462         
       
   463         if (i < twCx.tlOnDisplay.length - 1 && !(new Date(twCx.tlOnDisplay[i].end).valueOf() % 1800000)) {
       
   464             tlPaper.path("M0 "+posY+"L" + twCx.tlWidth +" "+posY).attr({"stroke":"#ccc"});
       
   465             tlPaper.text(2, posY, new Date(twCx.tlOnDisplay[i].end).toTimeString().substr(0,5)).attr({ "text-anchor" : "start", "font-size": "9px" });
       
   466         }
       
   467     }
       
   468     
       
   469     // Dessin de la correspondance liste-timeline
       
   470     
       
   471     var startY = twCx.tlHeight - startTl * twCx.scaleY,
       
   472         endY = twCx.tlHeight - ( endTl + 1 ) * twCx.scaleY;
       
   473     tlPaper.path("M0 " + twCx.tlHeight + "L" + twCx.deltaX + " " + startY + "L" + twCx.tlWidth + " " + startY + "L" + twCx.tlWidth + " " + endY + "L" + twCx.deltaX + " " + endY + "L0 0" )
       
   474         .attr({ "stroke" : "none", "fill" : "#000080", "opacity" : .1 });
       
   475     
       
   476     // dessin du tweet courant
       
   477         
       
   478     var posp = tlPosTweet(twCx.tweets[p]);
       
   479     if (posp) {
       
   480         
       
   481         drawTweetPos(posp, "#ffff00");
       
   482         
       
   483         // dessin des liens entre tweets
       
   484         
       
   485         function tweetAndArc(a, b, aorb) {
       
   486             if (a && b) {
       
   487                 drawTweetPos(aorb ? a : b, "#00c000");
       
   488                 tlPaper.path(arc(a,b))
       
   489                     .attr({ "stroke" : "#e000e0", "stroke-width" : 1.5 });
       
   490             }
       
   491         }
       
   492         
       
   493         if (twCx.tweets[p].retweeted_status) {
       
   494             var t = tweetById(twCx.tweets[p].retweeted_status.id);
       
   495             if (t) {
       
   496                 tweetAndArc(posp, tlPosTweet(t));
       
   497             }
       
   498         }
       
   499         
       
   500         if (twCx.tweets[p].in_reply_to_status_id) {
       
   501             var t = tweetById(twCx.tweets[p].in_reply_to_status_id);
       
   502             if (t) {
       
   503                 tweetAndArc(posp, tlPosTweet(t));
       
   504             }
       
   505         }
       
   506         
       
   507         if (twCx.tweets[p].backRefs) {
       
   508             for (var i in twCx.tweets[p].backRefs) {
       
   509                 var t = tweetById(twCx.tweets[p].backRefs[i].referenced_by_id);
       
   510                 if (t) {
       
   511                 tweetAndArc(tlPosTweet(t), posp, true);
       
   512                 }
       
   513             }
       
   514         }
       
   515         
       
   516     }
       
   517     
       
   518 }
       
   519 
       
   520 $(document).ready(function() {
       
   521     tlPaper = Raphael("timeline", twCx.tlWidth, twCx.tlHeight);
       
   522     socket = io.connect('http://' + document.location.hostname );
       
   523     socket.on("initial_data", function(data) {
       
   524         twCx.timeline = [];
       
   525         twCx.idIndex = [];
       
   526         twCx.tweets = [];
       
   527         for (var i in data.tweets) {
       
   528             addTweet(data.tweets[i]);
       
   529         }
       
   530         if (twCx.followLast) {
       
   531             twCx.position = twCx.tweets.length - 1;
       
   532         }
       
   533         updateDisplay();
       
   534     });
       
   535     socket.on("update", function(data) {
       
   536         for (var i in data.new_tweets) {
       
   537             addTweet(data.new_tweets[i]);
       
   538         }
       
   539         if (twCx.followLast) {
       
   540             twCx.position = twCx.tweets.length - 1;
       
   541         }
       
   542         updateDisplay();
       
   543     });
       
   544     
       
   545     
       
   546     $("#tweetlist").mousewheel(function(e, d) {
       
   547         wheelDelta += d;
       
   548         if (Math.abs(wheelDelta) >= 1) {
       
   549             twCx.position = Math.min( twCx.tweets.length - 1, Math.max(0, parseInt(wheelDelta) + twCx.position ) );
       
   550             twCx.followLast = (twCx.position == twCx.tweets.length - 1);
       
   551             updateDisplay();
       
   552             wheelDelta = 0;
       
   553         }
       
   554         return false;
       
   555     });
       
   556     $("#timeline").mousewheel(function(e, d) {
       
   557         wheelDelta += d;
       
   558         if (Math.abs(wheelDelta) >= 1) {
       
   559             if (wheelDelta > 0) {
       
   560                 tl = Math.min(twCx.date_levels.length - 1, twCx.timeLevel + 1);
       
   561             } else {
       
   562                 tl = Math.max(0, twCx.timeLevel - 1);
       
   563             }
       
   564             if (tl != twCx.timeLevel) {
       
   565                 twCx.timeLevel = tl;
       
   566                 updateDisplay();
       
   567             }
       
   568             wheelDelta = 0;
       
   569         }
       
   570         return false;
       
   571     });
       
   572     $("#timeline, #tweetlist").mouseout(function() {
       
   573         $("#hovertweet").hide();
       
   574     });
       
   575     $("#timeline").mousemove(function(evt) {
       
   576         var twid = tlIdFromPos(evt.offsetX, evt.offsetY);
       
   577         if (twid) {
       
   578             rolloverTweet(twid.id, twid.annotation);
       
   579         } else {
       
   580             $("#hovertweet").hide();
       
   581         }
       
   582     });
       
   583     $("#timeline").click(function(evt) {
       
   584         var twid = tlIdFromPos(evt.offsetX, evt.offsetY);
       
   585         if (twid) {
       
   586             selectTweet(twid.id);
       
   587         }
       
   588     });
       
   589 });