Skip to content

Instantly share code, notes, and snippets.

@dannycochran
Created June 1, 2013 03:49
Show Gist options
  • Save dannycochran/5689222 to your computer and use it in GitHub Desktop.
Save dannycochran/5689222 to your computer and use it in GitHub Desktop.

Revisions

  1. dannycochran created this gist Jun 1, 2013.
    6 changes: 6 additions & 0 deletions TwitterWordRiver.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    # Twitter Word River

    A d3.js visualization showing trending Twitter words during natural catastrophes by distance from the epicenter and over time.

    Built on top of bunkat's Swimlane Chart:
    http://bl.ocks.org/1962173
    565 changes: 565 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,565 @@
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">
    <title>A River of Twitter Data</title>
    <script src="http://d3js.org/d3.v2.min.js"></script>
    <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
    <script type="text/javascript" src="http://f.cl.ly/items/2U0A2k0d1O2e0F2Y143F/jquery-ui.js"></script>
    <script type="text/javascript" src="./tipsy.js"></script>

    <link rel="stylesheet" href="./style.css" type="text/css" />
    <link rel="stylesheet" href="./tipsy.css" type="text/css" />
    <link rel="stylesheet" href="http://code.jquery.com/ui/1.9.1/themes/base/jquery-ui.css" />

    </head>

    <body>

    <div>
    <h1>
    <img src="http://f.cl.ly/items/3S462I0S3v1M2F3d1L2V/twitter-bird-white-on-blue1.png"/>
    Activity, Hurricane Sandy
    <!-- play & pause buttons, hosted externally because Stanford doesn't seem to like these files on cgi-bin -->
    <span><img class='playbutton' type="button" value="Play" id="play" src="http://f.cl.ly/items/3O2M2V2w2a0j0s1I2I32/Play.png"/></span>
    <span><img class='playbutton' type="button" value="Stop" id="stop" style="display:none" src="http://f.cl.ly/items/3v452T2g0i0i1V1C113O/pause.png"/></span>
    </h1>
    </div>

    <script type="text/javascript">

    // distance bins
    var distanceBins = [["0-50 Miles","Southern New Jersey"],["51-110 Miles","Includes Manhattan"],["111-200 Miles","Includes Washington D.C."],["201-500 Miles","Entire East Coast"],["501-1000 Miles","Midwest Region (including Chicago)"],["1001-3000 Miles","Continental U.S."],["≥ 3000 Miles","Rest of the World"]];
    // for universal access, switchData contains the whole json file
    var jsonData;
    var jsonDataRT;
    var jsonDataFilter;
    var jsonDataRTFilter;
    var switchData;
    var checkData = 0;

    // these control the user's potential selection range for brushing, too large a difference yields a scrunched graph
    var max_extent_width = 10
    , min_extent_width = 5;

    // min/max values of d.positivity, d.freqBin, and d.objectivity
    var positivityDomain = [0.0163966168908, 0.0418471720818]
    , frequencyDomain = [1,4]
    , objectivityDomain = [0.923212665835, 0.965169775079];

    // vars for decreasing blueness (by increasing RG values) of heat maps
    var redOffset = 0;
    var greenOffset = 0;

    d3.json("./sandyData.json", function (json) {

    jsonData = json;
    jsonDataRT = json;
    switchData = json;
    jsonDataFilter = switchData.filter(function(d) {return d.isRT == 0});
    jsonDataRTFilter = switchData.filter(function(d) {return d.isRT == 1});
    switchData = jsonDataFilter;

    var margin = {top: 20, right: 15, bottom: 15, left: 105}
    , width = 960 - margin.left - margin.right
    , height = 500 - margin.top - margin.bottom
    , miniHeight = distanceBins.length * 12 + 50
    , mainHeight = height - miniHeight - 50;

    x = d3.scale.linear()
    .domain([0,164])
    .range([1, width]);
    x1 = d3.scale.linear().range([1, width]);

    var ext = d3.extent(distanceBins, function(d,i) { return i; });
    y1 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, mainHeight]);
    y2 = d3.scale.linear().domain([ext[0], ext[1] + 1]).range([0, miniHeight]);

    //heat maps for main and mini graph (sets blue (RG) B value)
    colorScale = d3.scale.quantize()
    .domain(positivityDomain)
    .range([0,75,150,225]); // color will be arranged into four quartiles with quantize

    // font size scale from 12 to 20
    wordSizeScale = d3.scale.quantize()
    .domain(frequencyDomain)
    .range([12,14,16,18]);

    // resizes fonts if user expands brush selection
    extentSizeScale = d3.scale.linear()
    .domain([min_extent_width,max_extent_width])
    .range([2,4]);

    // determines if bins are emotional enough to be italic
    typeFaceScale = d3.scale.linear()
    .domain(objectivityDomain)
    .range([0,3]);

    var chart = d3.select('body')
    .append('svg:svg')
    .attr('width', width + margin.right + margin.left+105)
    .attr('height', height + margin.top + margin.bottom)
    .attr('class', 'chart');

    chart.append('defs').append('clipPath')
    .attr('id', 'clip')
    .append('rect')
    .attr('width', width)
    .attr('height', mainHeight);

    main = chart.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
    .attr('width', width)
    .attr('height', mainHeight)
    .attr('class', 'main');

    mini = chart.append('g')
    .attr('transform', 'translate(' + margin.left + ',' + (mainHeight + 60) + ')')
    .attr('width', width)
    .attr('height', miniHeight)
    .attr('class', 'mini');

    // draw the distanceBins for the main chart
    main.append('g').selectAll('.laneLines')
    .data(distanceBins)
    .enter().append('line')
    .attr('x1', 0)
    .attr('y1', function(d,i) { return d3.round(y1(i)) + 1; })
    .attr('x2', width)
    .attr('y2', function(d,i) { return d3.round(y1(i)) + 1; })
    .attr('stroke', function(d,i) { return 'lightgray' });

    main.append('g').selectAll('.laneText')
    .data(distanceBins)
    .enter().append('text')
    .text(function(d) { return d[0]; })
    .attr('x',-5)
    .attr('y', function(d,i) { return y1(i + .5); })
    .attr('dy', '0.5ex')
    .attr('text-anchor', 'end')
    .attr('class', 'laneText')
    .attr('id', 'laneHover')
    .style('font-family', 'Rockwell')
    .on('mouseover',function() {
    $('svg #laneHover').tipsy({
    gravity: 'nw',
    html: true,
    title: function() {
    var d = this.__data__;
    return d[1];
    }
    })
    });

    // x-axis label
    main.append('text')
    .attr('class', 'x_label')
    .attr('id', 'x_label')
    .attr('text-anchor', 'end')
    .attr('x', width+70)
    .attr('y', mainHeight+5)
    .style('font-family', 'Rockwell')
    .text('hours from');
    main.append('text')
    .attr('class', 'x_label')
    .attr('id', 'x_label')
    .attr('text-anchor', 'end')
    .attr('x', width+70)
    .attr('y', mainHeight + 19)
    .style('font-family', 'Rockwell')
    .text('landfall');

    // sloppy legend - how does one wrap SVG text?
    main.append('svg:text')
    .attr('x', width+7)
    .attr('y', 15)
    .style('font-family', 'Rockwell')
    .style('font-size',12)
    .text('Lighter shades of');
    main.append('svg:text')
    .attr('x', width+7)
    .attr('y', 30)
    .style('font-family', 'Rockwell')
    .style('font-size',12)
    .text('blue correspond to');
    main.append('svg:text')
    .attr('x', width+7)
    .attr('y', 45)
    .style('font-family', 'Rockwell')
    .style('font-size',12)
    .text('higher positivity.');
    main.append('svg:text')
    .attr('x', width+7)
    .attr('y', 60)
    .style('font-family', 'Rockwell')
    .style('font-size',12)
    .text('(Sentiwordnet');
    main.append('svg:text')
    .attr('x', width+7)
    .attr('y', 75)
    .style('font-family', 'Rockwell')
    .style('font-size',12)
    .text('sentiment score)');

    // toggle tweets on and off
    main.append('rect')
    .attr('id','toggle')
    .attr('x', width+7)
    .attr('y', 88)
    .attr('width',90)
    .attr('height',20)
    .style('fill','#4099FF')
    .on('mouseup', switchAllData)
    .attr('rx',5)
    .attr('ry',5);
    main.append('svg:text')
    .attr('id', 'retweet-toggle')
    .attr('x', width+15)
    .attr('y', 102)
    .text('Retweets: Off') // Toggles between on and off
    .style('font-family', 'Rockwell')
    .style('fill','white')
    .style('font-size',12)
    .style('pointer-events','none');

    // draw the distanceBins for the mini chart
    mini.append('g').selectAll('.laneLines')
    .data(distanceBins)
    .enter().append('line')
    .attr('x1', 0)
    .attr('y1', function(d,i) { return d3.round(y2(i)) + 0.5; })
    .attr('x2', width)
    .attr('y2', function(d,i) { return d3.round(y2(i)) + 0.5; })
    .attr('stroke', function(d) { return d === '' ? 'white' : 'lightgray' });

    mini.append('g').selectAll('.laneText')
    .data(distanceBins)
    .enter().append('svg:text')
    .text(function(d) { return d[0]; })
    .attr('x', -10)
    .attr('y', function(d,i) { return y2(i+ 0.5); })
    .attr('dy', '0.5ex')
    .attr('text-anchor', 'end')
    .attr('class', 'laneText');

    // draw the x-axes
    xDateAxis = d3.svg.axis()
    .scale(x1)
    .ticks(20)
    .tickSize(15, 0, 0);

    x1DateAxis = d3.svg.axis()
    .scale(x)
    .ticks(20)
    .tickSize(15, 0, 0);

    main.append('g')
    .attr('transform', 'translate(0,' + mainHeight + ')')
    .attr('class', 'main axis')
    .call(xDateAxis);

    mini.append('g')
    .attr('transform', 'translate(0,125)')
    .attr('class', 'axis')
    .call(x1DateAxis)
    .selectAll('text')
    .attr('dx', 9)
    .attr('dy', 12);

    // declare the main rectangles to be amended in display()
    itemRects = main.append('g')
    .attr('clip-path', 'url(#clip)');

    // draw mini graph
    mini.append('g').selectAll('miniItems')
    .data(switchData)
    .enter().append('rect')
    .attr('class', function(d) { return 'miniItem '; })
    .attr("x",function(d,i) {return x(d.time_bin)})
    .attr("y",function(d) {return y2(d.dist_bin+.08)})
    .attr("width",x(2)-x(1))
    .attr("height",y2(1)*.5)
    .style('fill', function(d) {
    var b = 255-(Math.round(colorScale(d.positivity)));
    return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
    });

    // invisible rect for when brush is moved by click instead of drag
    mini.append('rect')
    .attr('pointer-events', 'painted')
    .attr('width', width)
    .attr('height', miniHeight)
    .attr('visibility', 'hidden')
    .on('mouseup', moveBrush);

    // draw the selection area
    brush = d3.svg.brush()
    .x(x)
    .extent([0,5])
    .on("brush", display)

    mini.append('g')
    .attr('class', 'x brush')
    .call(brush)
    .selectAll('rect')
    .attr('y', 1)
    .attr('id','brushHover')
    .attr('height', miniHeight - 1);

    mini.selectAll('rect.background').remove();
    display();

    });

    // check to see which dataset is currently displayed, and if button is clicked, switch dataset
    function switchAllData() {
    if (checkData == 0) {
    main.select('#retweet-toggle')
    .text('Retweets: On');
    main.select('#toggle')
    .transition().duration(500)
    .style('fill','#009900');
    checkData = 1;
    switchData = jsonDataRTFilter;
    } else {
    main.select('#retweet-toggle')
    .text('Retweets: Off');
    main.select('#toggle')
    .transition().duration(500)
    .style('fill','#4099FF');
    checkData = 0;
    switchData = jsonDataFilter;
    }
    display();
    }

    function display () {

    var rects, labels
    , minExtent = brush.extent()[0]
    , maxExtent = brush.extent()[1];
    var extentWidth = maxExtent - minExtent;
    if (extentWidth > max_extent_width)
    maxExtent = minExtent + max_extent_width;
    if (extentWidth < min_extent_width)
    maxExtent = minExtent + min_extent_width;
    var visItems = switchData.filter(function (d) {return d.time_bin <= maxExtent+1 && d.time_bin >= minExtent-1});

    mini.select('.brush').call(brush.extent([minExtent, maxExtent]));

    x1.domain([minExtent, maxExtent]);

    if ((maxExtent - minExtent) >= 10) {
    xDateAxis.ticks(10);
    }
    else {
    xDateAxis.ticks(5);
    }

    // update the axis

    main.select('.main.axis').call(xDateAxis)
    .selectAll('text')
    .attr('dx', 5)
    .attr('dy', 12);

    // upate the item rects
    rects = itemRects.selectAll('rect')
    .data(switchData, function (d,i) { return i; })
    .attr('x', function(d) { return x1(d.time_bin); })
    .attr('y', function(d) { return y1(d.dist_bin) + .1 * y1(1) + 0.5; })
    .attr('width', 165);

    rects.enter().append('svg:rect')
    .attr('x', function(d) { return x1(d.time_bin); })
    .attr('y', function(d) { return y1(d.dist_bin) + .1 * y1(1) + 0.5; })
    .attr('width', 165)
    .attr('class','borderMe')
    .attr('height', function(d) { return .8 * y1(1); })
    .style('fill', function(d) {
    var b = 255-(Math.round(colorScale(d.positivity)));
    return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
    });
    rects.exit().remove();

    // update the item labels
    labels = itemRects.selectAll('text')
    .data(visItems, function (d) { return d.word; })
    .text(function (d) { return d.word; })
    .attr('x', function(d) {
    return Math.round(x1(d.time_bin+0.05));
    })
    .attr('y', function(d) { return y1(d.dist_bin) + .6 * y1(1)})
    .attr('id','hover')
    .style('font-size', 14);

    labels.enter().append('svg:text')
    .text(function (d) { return d.word; })
    .attr('x', function(d) {
    return Math.round(x1(d.time_bin+0.05));
    })
    .attr('y', function(d) { return y1(d.dist_bin) + .6 * y1(1) })
    .attr('text-anchor', 'start')
    .attr('class', 'itemLabel')
    .attr('id','hover')
    .style('font-size', 14)
    .style('font-family', 'Arial')
    .style('fill','white')
    .on('mouseup',function(d) {
    var selectedWord = d.word;
    highlightWords(selectedWord);
    });

    labels.order();
    labels.exit().remove();

    function highlightWords (a) {
    mini.selectAll('miniItems')
    .data(switchData)
    .enter().append('rect')
    .attr('class', function(d) { return 'miniItem '; })
    .attr("x",function(d,i) {return x(d.time_bin)})
    .attr("y",function(d) {return y2(d.dist_bin+.08)})
    .attr("width",x(2)-x(1))
    .attr("height",y2(1)*.5)
    .style('fill', function(d) {
    var b = Math.round(colorScale(d.positivity));
    if (d.word == a) {
    return "rgb(0,200,0)";
    }
    else {
    var b = 255-(Math.round(colorScale(d.positivity)));
    return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
    }
    })
    rects = itemRects.selectAll('rect')
    .data(switchData)
    .transition().duration(1000)
    .style('fill', function(d) {
    var b = 255-(Math.round(colorScale(d.positivity)));
    if (d.word == a) {
    return "rgb(" + redOffset + ",200," + greenOffset + ")";
    }
    })
    .transition().delay(2000).style('fill', function(d) {
    var b = 255-(Math.round(colorScale(d.positivity)));
    return "rgb(" + redOffset + "," + greenOffset + "," + b + ")";
    });
    }

    // tipsy hover states by element ID
    $('svg #hover').tipsy({
    gravity: 'w',
    html: true,
    title: function() {
    var d = this.__data__;
    return d.sample_tweet;
    }
    });

    $('svg #x_label').tipsy({
    gravity: 'ne',
    html: true,
    title: function() {
    return "0 Hour is LandFall";
    }
    })

    $('svg #typeFace').tipsy({
    gravity: 'ne',
    html: true,
    title: function() {
    return "<span style='font-family: Arial; font-style: italic'>Italic is highly emotional,</span>" + "<br>" + "<span style='font-family: Arial'>No styling is a normal level of emotion.</span>" + "<br>" + "<span style='font-family: Arial Black; font-weight:bold'>Bold is removed of emotion.</span>";
    }
    })
    $('svg #color').tipsy({
    gravity: 'ne',
    html: true,
    title: function() {
    return "Lighter shades correlate to higher positivity. (Sentiwordnet sentiment score)";
    }
    })
    $('svg #frequency').tipsy({
    gravity: 'ne',
    html: true,
    title: function() {
    return "Larger fonts correlate to higher relative word frequency.";
    }
    })

    $('svg #brushHover').tipsy({
    gravity: 'sw',
    html: true,
    title: function() {
    return "Drag or expand me to scroll through data.";
    }
    })

    $('svg #toggle').tipsy({
    gravity: 'ne',
    html: true,
    fontSize: 10,
    title: function() {
    return "Click here to add or remove retweets from the data.";
    }
    })

    $('.tipsy:last').remove();
    };

    // for clicking brush to a location instead of dragging
    function moveBrush () {
    var origin = d3.mouse(this)
    , point = x.invert(origin[0])
    , halfExtent = (brush.extent()[1] - brush.extent()[0]) / 2
    , start = point - halfExtent
    , end = point + halfExtent;
    brush.extent([start,end]);
    display();
    }

    // Set up auto play parameters
    var timer = null,
    duration = 100,
    extentIncrement = 0.05,
    extentMax = 164;

    // Define behavior of the Play button
    $('#play').click(function() {
    var extent = brush.extent();
    if (extent[1] >= extentMax) {
    extent[1] -= extent[0];
    extent[0] = 0;
    brush.extent(extent);
    }
    display();
    if (timer) {
    clearInterval(timer);
    }
    timer = setInterval(function() {
    var extent = brush.extent();
    if (extent[1] <= extentMax) {
    extent[0] += extentIncrement;
    extent[1] += extentIncrement;
    display();
    brush.extent(extent);
    }
    else {
    $('#stop').click();
    }
    }, duration);
    $(this).hide();
    $('#stop').show();
    });

    $('#stop').click(function() {
    if (timer) {
    clearInterval(timer);
    }
    $(this).hide();
    $('#play').show();
    });

    </script>
    </body>
    </html>
    1 change: 1 addition & 0 deletions sandyData.json
    1 addition, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
    87 changes: 87 additions & 0 deletions style.css
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,87 @@
    svg {
    margin-top: -30px;
    }

    div h1 {
    padding: 15px 20px 20px;
    margin-top:15px;
    margin-left:105px;
    background: #4099FF;
    color: white;
    font-size: 56px;
    max-width:800px;
    text-align: right;
    font-family: Rockwell, "Courier New", Courier, Georgia,
    Times, "Times New Roman", serif;
    }

    div img {
    float:left;
    margin-top: 5px;

    }
    .chart {
    shape-rendering: crispEdges;
    }

    .mini text {
    font: 9px sans-serif;
    }

    .main text {
    font-family: Rockwell, "Courier New", Courier, Georgia,
    Times, "Times New Roman", serif;
    font: 12px sans-serif;
    }

    .month text {
    text-anchor: start;
    }

    .todayLine {
    stroke: blue;
    stroke-width: 1.5;
    }

    .axis line, .axis path {
    stroke: black;
    }

    .axis text {
    font-family: Rockwell;
    }

    .miniItem {
    stroke-width: 6;
    }

    .future {
    stroke: gray;
    fill: #ddd;
    }

    .past {
    stroke: green;
    fill: lightgreen;
    }

    .brush .extent {
    stroke: gray;
    fill: blue;
    fill-opacity: .165;
    }

    .laneText {
    font-family: Rockwell, "Courier New", Courier, Georgia, Times, "Times New Roman", serif;
    }

    .borderMe {
    border-style:solid;
    border-right:thick double #ff0000;
    }

    span img {
    float:right;
    margin-right: -130px;
    margin-top: -15px;
    }
    25 changes: 25 additions & 0 deletions tipsy.css
    Original file line number Diff line number Diff line change
    @@ -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: left; font-family: Rockwell; }

    /* 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; }
    258 changes: 258 additions & 0 deletions tipsy.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,258 @@
    // tipsy, facebook style tooltips for jquery
    // version 1.0.0a
    // (c) 2008-2010 jason frame [[email protected]]
    // 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};
    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 = $('<div class="tipsy"></div>').html('<div class="tipsy-arrow"></div><div class="tipsy-inner"></div>');
    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);