(function ($) {
    "use strict";

    var tooltip,
        Mixpanel;

    function addCommasToNum(numString) {
        var rgx = /(\d+)(\d{3})/,
            x = numString;
        while (rgx.test(x)) {
            x = x.replace(rgx, '$1' + ',' + '$2');
        }
        return x;
    }

    // Enable creation of arbitrary hitboxes on screen
    // to fire a callback on mouseenter/mouseleave.
    function bindHitbox(eventType, hitbox, callback) {
        var handler;
        if (eventType == "enter") {
            handler = function (e) {
                var x = e.pageX, y = e.pageY;
                if (x > hitbox.x1 && x < hitbox.x2 && y > hitbox.y1 && y < hitbox.y2) {
                    callback();
                    $(window).unbind("mousemove", handler);
                } 
            }
        } else if (eventType == "leave") {
            handler = function (e) {
                var x = e.pageX, y = e.pageY;
                if (x < hitbox.x1 || x > hitbox.x2 || y < hitbox.y1 || y > hitbox.y2) {
                    callback();
                    $(window).unbind("mousemove", handler);
                } 
            }
        }
        $(window).mousemove(handler);
    }

    function hitBox(svgNode) {
        var bbox = svgNode.getBBox(),
            tMatrix = svgNode.getScreenCTM(),
            xa = tMatrix.e + tMatrix.a * bbox.x,
            xb = xa + tMatrix.a * bbox.width,
            ya = tMatrix.f + tMatrix.d * bbox.y,
            yb = ya + tMatrix.d * bbox.height,
            x1 = Math.min(xa, xb),
            x2 = Math.max(xa, xb),
            y1 = Math.min(ya, yb),
            y2 = Math.max(ya, yb),
            width = x2 - x1,
            height = y2 - y1,
            xc = x1 + width / 2,
            yc = y1 + height / 2;
        return {
            x1: x1,
            x2: x2,
            y1: y1,
            y2: y2,
            xc: xc,
            yc: yc,
            width: width,
            height: height
        };
    }

    function rectangleCollision(r1, r2) {
        var xC = r1.x1 < r2.x2 && r2.x1 < r1.x2,
            yC = r1.y1 < r2.y2 && r2.y1 < r1.y2;
        return xC && yC;
    }

    function niceDay(time) {
        var date = new Date(time || 0),
            now = new Date(),
            diff, day_diff;
        now.setHours(0)
        now.setMinutes(0)
        now.setSeconds(0)
        now.setMilliseconds(0);
        diff = (now.getTime() + 864E5 - date.getTime()) / 1000;
        day_diff = Math.floor(diff / 86400);
              
        if (isNaN(day_diff)) {
            return;
        }
                
        return day_diff == 0 && "Today " ||
                day_diff == 1 && "Yesterday " ||
                day_diff > 1 && d3.time.format('%A %m/%d')(date) + " ";
    }

    function tweetUserLink(dict) {
        return '<a href="http://twitter.com/' + dict.username + '" target="_blank" class="tweet_user_link">' + dict.username + '</a>';
    }

    function tweetUserImage(dict) {
        return '<a href="http://twitter.com/' + dict.username + '" target="_blank" class="mt_2 left tweet_user_link"><img src="' + dict.image + '" width=40 /></a>';
    }

    function tweetLink(dict) {
        return '<a href="http://twitter.com/' + dict.username + '/status/' + dict.id + '" target="_blank" class="weak faint small livetime tweet_link" data-time="' + dict.time * 1000 + '">' + relativeTime((dict.time || 0) * 1000) + '</a>';
    }

    function linkify(text) {
        var exp = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
        return text.replace(exp, "<a href='$1' target='_blank' class='url'>$1</a>");
    }

    function formatShortNum(num) {
        if (num < 1000) {
            return Math.round(num);
        } else if (num < 10000) {
            return Math.round(num / 100) / 10 + 'k';
        } else {
            return Math.round(num / 1000) + 'k';
        }
    }

    function formatDuration(time) {
        var days = Math.floor(time / 86400);

        if (isNaN(time)) {
            return;
        }
                
        return days == 0 && (
                        time < 60 && Math.floor(time) + " seconds" ||
                        time < 120 && "1 minute" ||
                        time < 3600 && Math.floor( time / 60 ) + " minutes" ||
                        time < 4000 && "1 hour" ||
                        time < 86400 && Math.floor( time / 360 ) / 10 + " hours") ||
                days >= 1 && "1 day";
    }

    function relativeTime(time) {
        var date = new Date(time || 0),
            diff = (((new Date()).getTime() - date.getTime()) / 1000),
            day_diff = Math.floor(diff / 86400);
              
        if (isNaN(day_diff)) {
            return;
        } else if (day_diff < 0) {
            return 'just now';
        }
                
        return day_diff == 0 && (
                        diff < 60 && Math.floor(diff) + " seconds ago" ||
                        diff < 120 && "1 minute ago" ||
                        diff < 3600 && Math.floor( diff / 60 ) + " minutes ago" ||
                        diff < 7200 && "1 hour ago" ||
                        diff < 86400 && Math.floor( diff / 3600 ) + " hours ago") ||
                day_diff == 1 && "Yesterday" ||
                day_diff < 7 && day_diff + " days ago" ||
                day_diff < 31 && Math.ceil( day_diff / 7 ) + " weeks ago" ||
                day_diff >= 31 && "forever ago";
    }

    function relativeDays(time) {
        var date = moment(time),
            base = moment().milliseconds(0).seconds(0).minutes(0).hours(0).add("days", 1),
            base = moment(),
            diff = (base.valueOf() - date.valueOf()) / 1000,
            day_diff = Math.floor(diff / 86400);
              
        if (isNaN(day_diff)) {
            return;
        } else if (day_diff < 0) {
            return 'just now';
        }
                
        return day_diff == 0 && "Today" ||
                day_diff == 1 && "Yesterday" ||
                day_diff > 1 && day_diff + " days ago";
    }

    function relativeDaysQ(time) {
        var date = moment(time),
            base = moment().milliseconds(0).seconds(0).minutes(0).hours(0).add("days", 1),
            diff = (base.valueOf() - date.valueOf()) / 1000,
            day_diff = Math.floor(diff / 86400);
              
        if (isNaN(day_diff)) {
            return;
        } else if (day_diff < 0) {
            return 'just now';
        }
                
        return day_diff == 0 && "Today" ||
                day_diff == 1 && "Yesterday" ||
                day_diff > 1 && day_diff + " days ago";
    }

    function initMixpanel(ip) {
        if (ip !== '76.102.113.7' && ip !== '127.0.0.1' && ip.substr(0, 7) !== '192.168') {
            Mixpanel = mpq;
            console.log('Mixpanel TRACKING');
        } else {
            Mixpanel = {'track': function () {}};    
            console.log('Mixpanel NOT TRACKING');
        }
    }

    function Init(tid, ip) {
        tooltip = Tooltip();
        window.Widgets = {
            Graph: Graph("#graph_container")
        }
        window.Widgets.Top = window.Widgets.Graph.Top("#graph_left_content");
        window.Data = {};
        window.Data[tid] = {
            Volume: TimelineData(),
            Top: TopData() 
        }
        window.Widgets.Graph.subscribe(window.Data[tid].Volume);
        window.Widgets.Top.subscribe(window.Data[tid].Top);
        function initThis() {
          if (window.Init == undefined) {
            setTimeout(initThis, 50);
          } else {
            window.Init();
            poll_tornado();
            window.Widgets.Graph.setHash(window.location.hash.slice(1));
          }
        }
        initThis();
        initMixpanel(ip);
        Mixpanel.track('TOPIC: ' + tid);
    }

    function Graph(container) {

        // Globals
        var series = {},
            H = height(),
            W = width(),
            svg = d3.select(container).append("svg:svg")
                .attr("class", "moki-svg"),
            g = svg.append("svg:g")
                .data([{x: 0, y: 1}])
                .attr("class", "moki-master-group"),
            lc = $("#graph_left"),
            rc = $("#graph_right"),
            lcc = $("#graph_left_content"),
            layers = {
                bg: g.append("svg:rect")
                            .attr("class", "moki-chart")
                            .attr("fill", "#f0f2f5")
                            .attr("transform", function (d) { return "translate(" + d.x + ", 0)"}),
                xaxis: g.append("svg:g")
                            .attr("class", "moki-chart-x-axis"),
                series: g.append("svg:g")
                            .attr("class", "moki-chart-series"),
                yaxis: g.append("svg:g")
                            .attr("class", "moki-chart-y-axis"),
                hl: g.append("svg:g")
                            .attr("class", "moki-chart-hl")
            },
            axes = {
                x: xAxis(),
                y: yAxis()
            },
            legend = Legend(),
            calendar = Calendar(),
            seriesTypes = {
                line: LineSeries,
                bar: BarSeries,
                point: PointSeries
            },
            range = 86400 * 3,
            future = 10800,
            masterMax = 0,
            masterMin = null,
            activeMax = future,
            activeMin = activeMax - range,
            yMax1, yMax2,
            nYMax1, nYMax2,
            X, Y, Y2,
            realtime = true;
        
        initChart();

        initSeries(0, 'bar');
        initSeries(1, 'line');
        initSeries(2, 'point', 1);
        
        bindDrag();
        bindResize();
        bindRealtime();
        bindKeyboard();

        function isValidHash(hash) {
            return hash != '' && !isNaN(hash);
        }

        function setHash(hash) {
            if (isValidHash(hash)) {
                jumpTo(+hash); 
            }
        }

        function updateActiveMax(m) {
            activeMax = Math.round(m);
            activeMin = activeMax - range;
        }

        function updateActiveMin(m) {
            activeMin = Math.round(m);
            activeMax = activeMin + range;
        }

        function initSeries(sid, type, host) {
            series[sid] = {
                masterData: [],
                activeData: [],
                series: seriesTypes[type](sid, host)
            };
        }

        function height() {
            return Math.max($(window).height() - 92, 100);
        }

        function width() {
            return Math.max($(window).width() - 495, 100);
        }

        function bindResize() {
            $(window).resize(function () {
                H = height();
                W = width();
                initChart();
                selectActiveData();
                rescale();
                redraw();
                //legend.slideX(0);
            });
        }

        function bindKeyboard() {
            $(document)
                .bind('keydown', 'right', function () {
                    jumpTo(activeMin + 21600);
                })
                .bind('keydown', 'left', function () {
                    jumpTo(activeMin - 21600);
                })
                .bind('keydown', 'ctrl+right', function () {
                    jumpTo(activeMin + range);
                })
                .bind('keydown', 'ctrl+left', function () {
                    jumpTo(activeMin - range);
                })
        }

        function displayTime(offset) {
            var offset = offset || 0,
                s = X.invert(-1 * offset),
                e = s + range,
                format = function (d) {
                    d = d * 1000;
                    return relativeDays(d);
                    return niceDay(d) + d3.time.format("%I:%M %p")(
                        new Date(d)
                        );
                };
            $('#windowStartTime').html(
                format(s)
            );
        }

        function shortenText(t, limit) {
            var st = '', words, i = 0;
            if (t.length < limit * 1.25) {
                return t;
            }
            words = t.split(" ");
            while (st.length + words[i].length + 1 < limit || st == '') {
                st += words[i] + " ";
                i++;
            }
            return st.slice(0, -1) + '...';
        }

        function updateLiveStatus() {
            if (realtime) {
                $('#realtime').addClass("live");
            } else {
                $('#realtime').removeClass("live");
            }
        }

        function initChart() {
            svg
                .attr("width", W)
                .attr("height", H);
            layers.bg
                .attr("width", W)
                .attr("height", H);
            lc.height(H+1);
            rc.height(H+1);
            lcc.height(H - 240);
        }

        function goRealtime() {
            jumpTo(masterMax - range + future);
        }

        function jumpTo(date) {
            var oldActiveMin = activeMin,
                diff;
            activeMin = Math.max(
                          Math.min(
                            date,
                            masterMax - range + future
                          ),
                          masterMin - future
                        );
            activeMax = activeMin + range;
            diff = Math.abs(oldActiveMin - activeMin);
            selectActiveData();
            rescale();
            redraw((diff < range));
            if (activeMax >= masterMax) {
                realtime = true;
            } else {
                realtime = false;
            }
            updateLiveStatus();
        }

        function bindRealtime() {
            $('#realtime').live('click', function () {
                var $this = $(this),
                    isLive = $this.hasClass("live");
                if (!isLive) {
                    goRealtime();
                    Mixpanel.track('go REALTIME');
                } else {
                    Mixpanel.track('attempt to turn realtime off');
                }
            });
        }

        function Calendar() {
            var activeM,
                firstM, lastM;

            function cID(month) {
                return month.year() + '_' + (month.month() + 1);
            }

            $('#calendar_nav .left').click(function () {
                var YM = activeM.split('_'),
                    mt = moment([YM[0], YM[1] - 1]);
                activeM = cID(mt.subtract('months', 1));
                displayActiveMonth();
            });
            $('#calendar_nav .right').click(function () {
                var YM = activeM.split('_'),
                    mt = moment([YM[0], YM[1] - 1]);
                activeM = cID(mt.add('months', 1));
                displayActiveMonth();
            });
            $('#calendar_panes .calendar_day.active').live('click', function () {
                var YMD = $(this).attr('id').split('-')[1].split('_'),
                    mt = moment([YMD[0], YMD[1] - 1, YMD[2]]),
                    v = mt.valueOf() / 1000;
                jumpTo(v);
                Mixpanel.track('Calendar CLICK');
            });

            function update() {
                var am = moment(activeMin * 1000),
                    aM = moment(activeMax * 1000 - 1),
                    days = rangeDays(am, aM),
                    dID = function (d) { return cID(d) + '_' + d.date(); },
                    i, j, m, maxM = [0, ''],
                    mFrequency = {};
                $('#calendar_panes .current_day').removeClass("current_day");
                for (i = 0; i < days.length; i++) {
                    var rDays = $('#calendar_panes').find('.' + dID(days[i]));
                    rDays.addClass("current_day");
                    for (j = 0; j < rDays.length; j++) {
                        var key = $(rDays[j]).attr("id").split("-")[0];
                        if (mFrequency[key] == undefined) {
                            mFrequency[key] = 0;
                        }
                        mFrequency[key] += 1;
                    }
                }
                for (m in mFrequency) {
                    if (mFrequency[m] > maxM[0]) {
                        maxM = [mFrequency[m], m];
                    }
                }
                if (maxM[1] != activeM) {
                    activeM = maxM[1];
                    displayActiveMonth();
                }
            }

            function displayActiveMonth() {
                var mSplit = activeM.split('_');
                $('#calendar_panes').children().hide();
                $('#calendar_panes').find('#' + activeM).show();
                $('#calendar_title').html(moment([mSplit[0], mSplit[1] - 1]).format("MMMM YYYY"));
                if (activeM == firstM) {
                    $('#calendar_nav .left').hide();
                } else {
                    $('#calendar_nav .left').show();
                }
                if (activeM == lastM) {
                    $('#calendar_nav .right').hide();
                } else {
                    $('#calendar_nav .right').show();
                }
            }

            function redraw() {
                var am = moment(masterMin * 1000).hours(0).minutes(0).seconds(0).milliseconds(0),
                    aM = moment(masterMax * 1000).hours(0).minutes(0).seconds(0).milliseconds(0),
                    em = am.clone().date(1),
                    eM = aM.clone().add("months", 1).date(0),
                    months = enumerateMonths(am, aM),
                    display,
                    i;
                $('#calendar_panes').html('');
                console.log(months);
                console.log('hi');
                for (i = 0; i < months.length; i++) {
                    if (i == months.length - 1) {
                        $('#calendar_title').html(months[i].format('MMMM YYYY'));
                        display = true;
                        activeM = lastM = cID(months[i]);
                        $('#calendar_nav').data('month', months[i]);
                    };
                    $('#calendar_panes').append(calendarPaneHTML({
                        month: months[i],
                        days: enumerateDays(months[i])
                    }, display, am, aM));
                }
                firstM = cID(months[0]);
                displayActiveMonth();
            }

            function calendarPaneHTML(pane, display, am, aM) {
                var html = '', i;
                html += '<div id="' + cID(pane.month) + '" class="clearfix';
                if (!display) {
                    html += ' hide';
                }
                html += '">';
                for (i = 0; i < pane.days.length; i++) {
                    var dID = cID(pane.days[i]) + '_' + pane.days[i].date();
                    html += '<div id="' + (cID(pane.month) + "-" + dID) + '" class="' + dID + ' calendar_day';
                    if (pane.days[i].month() != pane.month.month()) {
                        html += ' out_month';
                    }
                    if (am <= pane.days[i] && pane.days[i] <= aM) {
                        html += ' active';
                    }
                    html += '">' + pane.days[i].date() + '</div>';
                }
                html += '</div>';
                return html;
            }

            function enumerateMonths(m, M) {
                var months = [],
                    d = m.clone(),
                    D = M.clone();
                d.date(1),
                D.add("months", 1).date(0);
                while (d <= D) {
                    months.push(d);
                    d = d.clone().add("months", 1);
                }
                return months;
            }

            function enumerateDays(m) {
                var days = [],
                    mS = m.clone().date(1),
                    mE = m.clone().add("months", 1).date(0);
                roundBackWeek(mS);
                roundForwardWeek(mE);
                while (mS <= mE) {
                    days.push(mS);
                    mS = mS.clone().add("days", 1);
                }
                return days;
            }

            function rangeDays(m, M) {
                var d = m.clone(),
                    D = M.clone(),
                    days = [];
                while (d < D) {
                    days.push(d);
                    d = d.clone().add("days", 1);
                }
                d.add("days", -1);
                if (d.date() != D.date()) {
                    days.push(D);
                }
                return days;
            }

            function roundBackWeek(mt) {
                mt.subtract('days', mt.day());
            }

            function roundForwardWeek(mt) {
                mt.add('days', 6 - mt.day());
            }

            return {
                redraw: redraw,
                update: update
            };
        }

        function changeWindow(index) {
            var windowSizes = {
                0: 86400,
                1: 21600
            }
            range = windowSizes[index];
            activeMin = activeMax - range;
            selectActiveData();
            rescale();
            redraw();
        }

        function bindDrag() {
            g.call(d3.behavior.drag()
                    .on("dragstart", dragStart)
                    .on("drag", drag)
                    .on("dragend", dragEnd));

            function dragStart(d) {
                g.style("cursor", "move");
                this.__origin__ = d.x;
                this.__softmax__ = W - X(masterMax);
                this.__max__ = W - X(masterMax + future);
                this.__min__ = -1 * X(masterMin - future);
            }

            function drag(d) {
                d.x = Math.min(
                        Math.max(
                          Math.max(
                            this.__origin__ - W,
                            Math.min(
                              d.x + d3.event.dx,
                              this.__origin__ + W
                            )
                          ),
                          this.__max__
                        ),
                        this.__min__
                      );
                g.attr("transform", "translate(" + Math.round(d.x) + ",0)");
                axes.y.slideX(Math.round(d.x));
                //legend.slideX(Math.round(d.x));
                displayTime(d.x);
                var crealtime = realtime;
                if (d.x <= this.__softmax__) {
                    realtime = true;
                } else {
                    realtime = false;
                }
                if (crealtime != realtime) {
                    updateLiveStatus();
                }
                if (d.x == this.__max__) {
                    futureFlash(this);
                }
                if (d.x == this.__min__) {
                    pastFlash(this);
                }
            }

            function dragEnd(d) {
                g.style("cursor", "default");
                removeFlashes();
                delete this.__futureflash__;
                delete this.__pastflash__;
                if (Math.abs(d.x) > 0) {
                    updateActiveMin(X.invert(-1 * d.x));
                    axes.y.slideX(0);
                    //legend.slideX(0);
                    d.x = 0;
                    delete this.__origin__;
                    delete this.__max__;
                    selectActiveData();
                    rescale();
                    redraw();
                    Mixpanel.track('Drag');
                };
            }
        }

        function clear(sid) {
            series[sid].masterData = [];
            series[sid].activeData = [];
            rescale();
            redraw();
        }

        function futureFlash(ctx) {
            if (!ctx.__futureflash__) {
                ctx.__futureflash__ = true;
                layers.hl.append("svg:rect")
                  .attr("x", X(masterMax))
                  .attr("height", Y(65))
                  .attr("width", 500)
                  .classed("moki-future-flash", 1)
                  .attr("fill", "rgb(245, 245, 100)")
                  .style("opacity", 0)
                  .transition().duration(1000)
                  .style("opacity", 0.3);
                  //.attr("fill", "rgba(245, 245, 100, 0.3)");
                var t = layers.hl.append("svg:text")
                            .classed("moki-future-flash", 1)
                            .text("The Future")
                            .style("font-size", 13)
                            .style("font-weight", "bold"),
                    bbox = t.node().getBBox();

                t
                  .attr("transform", "rotate(270)")
                  .attr("x", -1 * (H / 2 + bbox.width / 2))
                  .attr("y", X(masterMax + 5400) + bbox.height / 2)
                  .attr("fill", "rgba(120, 120, 120, 0)")
                  .transition().duration(1000)
                  .attr("fill", "rgba(120, 120, 120, 1)");
            }
        }

        function removeFlashes() {
            layers.hl.selectAll(".moki-future-flash").remove(); 
            layers.hl.selectAll(".moki-past-flash").remove(); 
        }

        function pastFlash(ctx) {
            if (!ctx.__pastflash__) {
                ctx.__pastflash__ = true;
                layers.hl.append("svg:rect")
                  .attr("x", X(masterMin) - 500)
                  .attr("height", Y(65))
                  .attr("width", 500)
                  .classed("moki-past-flash", 1)
                  .attr("fill", "rgb(245, 245, 100)")
                  .style("opacity", 0)
                  .transition().duration(1000)
                  .style("opacity", 0.3);
                  //.attr("fill", "rgba(245, 245, 100, 0.3)");
                var t = layers.hl.append("svg:text")
                            .classed("moki-future-flash", 1)
                            .text("No Data Before This Time")
                            .style("font-size", 13)
                            .style("font-weight", "bold"),
                    bbox = t.node().getBBox();

                t
                  .attr("transform", "rotate(270)")
                  .attr("x", -1 * (H / 2 + bbox.width / 2))
                  .attr("y", X(masterMin - 5400) + bbox.height / 2)
                  .attr("fill", "rgba(120, 120, 120, 0)")
                  .transition().duration(1000)
                  .attr("fill", "rgba(120, 120, 120, 1)");
            }
        }

        // coordinateList = [[x0, y0], [x1, y1], [x2, y2], ...]
        function update(sid, coordinateList) {
            var i,
                d = series[sid].masterData,
                offset = activeMax - masterMax,
                cMasterMax = masterMax;
            for (i = 0; i < coordinateList.length; i++) {
                var x = coordinateList[i][0],
                    y = coordinateList[i][1],
                    z = coordinateList[i][2],
                    key = d3.bisectLeft(d, [x, undefined]); // find insertion point
                if (key < d.length && d[key][0] == x) {
                    if (y == null) {
                        // delete existing point
                        d.splice(key, 1);
                    } else {
                        // update existing point
                        d[key][1] = y;
                        d[key][2] = z;
                    }
                // insert new point
                } else if (y != null) {
                    d.splice(key, 0, [x, y, z]);
                } else {
                    continue;
                }
                if (x > masterMax) {
                    masterMax = x;
                    if (realtime) {
                        activeMax = x + offset;
                        activeMin = activeMax - range;
                    }
                }
                if (x < masterMin || masterMin == null) {
                    masterMin = x;
                }
            }
            if (moment(masterMax * 1000).date() != moment(cMasterMax * 1000).date() || cMasterMax == 0) {
                calendar.redraw();
            }
            if (realtime) {
                selectActiveData();
                rescale();
                redraw();
                calendar.update();
            }

            // temporary client-side notable
            if (sid == 2) {
                Widgets.Top.update();
            }
        }

        function extendedMin(count) {
            return activeMin - (count || 1) * range;
        }

        function extendedMax(count) {
            return activeMax + (count || 1) * range;
        }

        function inRange(min, val, max) {
            return (min <= val && val <= max);
        }

        function inStrictRange(min, val, max) {
            return (min < val && val < max);
        }

        /* Select active sub-sequences of each series.
         * Determine yMax over all series.
         */
        function selectActiveData() {
            var em = extendedMin(),
                eM = extendedMax(),
                sid,
                i,
                eYMax = null;
            yMax1 = yMax2, yMax2 = null;
            for (sid in series) {
                var d = series[sid].masterData,
                    startIndex = d3.bisectLeft(d, [em, undefined]),
                    newActiveData = [];
                for (i = startIndex; i < d.length; i++) {
                    var x = d[i][0],
                        y = d[i][1],
                        z = d[i][2];
                    if (x > eM) {
                        break;
                    }
                    if (inRange(activeMin, x, activeMax) && (yMax2 < y || yMax2 == null)) {
                        yMax2 = y;
                    }
                    if (eYMax < y || eYMax == null) {
                        eYMax = y;
                    }
                    newActiveData.push([x, y, z]);
                }
                series[sid].activeData = newActiveData;
            }
            if (yMax2 == null || yMax2 == 0) {
                yMax2 = eYMax;
            }
        }

        function redraw(transition) {
            var sid;
            axes.x.redraw();
            axes.y.redraw(transition);
            g.attr("transform", "translate(0, 0)");
            for (sid in series) {
                series[sid].series.redraw(transition);
            }
            displayTime();
            calendar.update();
        }

        function BarSeries(sid) {
            var barGroup = layers.series.append("svg:g")
                .attr("class", "moki-bar-series-" + sid)
                .attr("transform", "scale(1, -1)"),
                interval = 800;
            function redraw() {
                return;
                var bars = barGroup.selectAll(".moki-bar")
                  .data(series[sid].activeData);

                bars
                  .enter().append("svg:rect")
                      .attr("transform", function (d) { return "translate(" + X(d[0] - interval / 2) + "," + -1 * H + ")"; })
                      .attr("width", function (d) { return X(d[0]) - X(d[0] - interval);})
                      .attr("height", function (d) { return H - Y(d[1] / 4); })
                      .attr("fill", "#d8dadd")
                      .attr("class", "moki-bar")
                      .on("mouseover", function (d) {
                          var $this = d3.select(this);
                          $this.each(showTooltip);
                          this.__color__ = $this.attr("fill");
                          $this.attr("fill", "#ccced1");
                      })
                      .on("mouseout", function (d) {
                          if (d[2] == undefined || !("entities" in d)) {
                              d3.select(this).attr("fill", this.__color__);
                          }
                      });

                if (yMax1 != null) {
                    bars
                      .attr("transform", function (d) { return "translate(" + X(d[0] - interval / 2) + "," + -1 * H + ")";})
                      .attr("width", function (d) { return X(d[0]) - X(d[0] - interval);})
                      .attr("height", function (d) { return H - Y2(d[1] / 4); })
                      .transition().ease("quad").duration(250)
                      .attr("height", function (d) { return H - Y(d[1] / 4); })
                } else {
                    bars
                      .attr("transform", function (d) { return "translate(" + X(d[0] - interval/2) + "," + -1 * H + ")";})
                      .attr("width", function (d) { return X(d[0]) - X(d[0] - interval);})
                      .attr("height", function (d) { return H - Y(d[1] / 4); })
                }
                bars.exit().remove();
            }

            function showTooltip(datum) {
                var html = '',
                    hitbox = hitBox(this),
                    $this = d3.select(this),
                    d = datum[2];

                if (d == undefined || !("entities" in d)) {
                    return;
                }

                html += '<div id="POI_tooltip_body" class="tooltip">';
                html += '<div class="tooltip_header clearfix"><div class="left">';
                html += '<span class="strong">Top Tweet</span>&nbsp; ';
                html += '</div></div>';
                html += '<div class="tooltip_body">';
                html += '<div class="conv_tooltip tooltip_inner">';
                $.each(d.entities.slice(0, 1), function (i, e) {
                    if (!e) {
                        return;
                    }
                    var dict = e[1];
                    if (i > 0) {
                        html += "<hr />";
                    }
                    // a tweet
                    if (e[0] == 0) {
                        html += tweetUserImage(dict);
                        html += '<div class="right" style="width: 294px;">';
                        html += '<p class="medium strong">' + tweetUserLink(dict) + ' &nbsp;' + tweetLink(dict);
                        if (dict.retweets != 0 && dict.retweets != '0' && dict.retweets != undefined) {
                            html += ' &nbsp;<img src="/images/icons/rt.png" /> <span class="weak faint tiny">' + dict.retweets + ' retweets</span>';
                        }
                        html += '</p>';
                        html += '<p class="small mt_2">' + linkify(dict.body) + '</p>';
                        html += '<p class="tiny mt_3 faded">(1 of ' + (dict.number != undefined ? dict.number : 1) + ' related conversations)</p>';
                        html += '</div><div style="clear: both;"></div>';
                    // an article
                    } else {
                        html += '<p class="strong small"><span style="color: #d92b42;">HEADLINE: </span><a href="' + dict.link + '" target="_blank" class="headline_link">' + dict.title;
                        html += '</a> <span class="faded weak">(' + dict.domain + ')</span></p>';
                    }
                });
                html += '</div>';
                html += '</div>';
                html += '</div>';
                
                tooltip.setContent(hitbox.xc, hitbox.y1, html, "top")
                        .addHandler(function () {
                          $this.attr("fill", $this.node().__color__);
                        })
                        .show($(this));
            }
            return {
                redraw: redraw
            }
        }

        function LineSeries(sid) {
            var lineGroup = layers.series.append("svg:g")
                .attr("class", "moki-line-series-" + sid);

            function redraw(transition) {
                var linePath = d3.svg.line()
                        .x(function (d, i) { return X(d[0]) + 0.5; })
                        .y(function (d) { return Y(d[1]) + 0.5; }),
                    fillPath = d3.svg.area()
                        .x(function (d, i) { return X(d[0]) + 0.5; })
                        .y1(function (d) { return Y(d[1]) + 0.5; })
                        .y0(function (d) { return H; }),
                    l = lineGroup.selectAll("path.moki-line-" + sid)
                          .data([series[sid].activeData]),
                    f = lineGroup.selectAll("path.moki-fill-" + sid)
                          .data([series[sid].activeData]);
                if (transition == undefined) {
                    transition = true;
                }
                
                f
                    .enter().append("svg:path")
                    .attr("d", fillPath)
                    .attr("fill", "rgba(2, 151, 164, 0.2)")
                    .attr("fill", "rgba(0, 0, 0, 0.1)")
                    .attr("fill", "rgba(38, 72, 77, 0.1)")
                    .attr("class", "moki-fill-" + sid);
                l
                    .enter().append("svg:path")
                    .attr("d", linePath)
                    .attr("stroke", "#2B97A4")
                    .attr("stroke-width", 5)
                    .attr("fill", "none")
                    .attr("class", "moki-line-" + sid);

                if (activeMax >= masterMax - range) {
                    var tip = lineGroup.selectAll("circle.moki-tip"),
                        tipData = series[sid].activeData[series[sid].activeData.length - 1];
                    if (!tip.node()) {
                        tip = lineGroup.append("svg:circle")
                                  .classed("moki-tip", 1);
                    }
                    tip
                      .attr("cx", X(tipData[0]))
                      .attr("r", 3.5)
                      .attr("fill", "#cc1414");
                    var whiteTip = function () {
                        tip.transition().duration(750).ease("sin")
                            .attr("fill", "#537680")
                            .delay(500)
                            .each("end", redTip);
                    };
                    var redTip = function () {
                        tip.transition().duration(750).ease("sin")
                            .attr("fill", "#b31b1b")
                            .each("end", whiteTip);
                    };
                } else {
                    lineGroup.selectAll("circle.moki-tip").remove();
                }

                if (yMax1 != null && nYMax1 != nYMax2 && transition) {
                    var oldLine = d3.svg.line()
                                      .x(function (d, i) { return X(d[0]) + 0.5; })
                                      .y(function (d) { return Y2(d[1]) + 0.5; }),
                        oldFill = d3.svg.area()
                                      .x(function (d, i) { return X(d[0]) + 0.5; })
                                      .y1(function (d) { return Y2(d[1]) + 0.5; })
                                      .y0(function (d) { return H; });
                    l.attr("d", oldLine)
                      .transition().ease("quad").duration(500)
                      .attr("d", linePath);
                    f.attr("d", oldFill)
                      .transition().ease("quad").duration(500)
                      .attr("d", fillPath);
                    if (activeMax >= masterMax - range) {
                        tip.attr("cy", Y2(tipData[1]))
                          .transition().ease("quad").duration(500)
                          .attr("cy", Y(tipData[1]))
                          .each("end", whiteTip);
                    }
                } else {
                    // kill in-progress transitions
                    l.transition().delay(0);
                    f.transition().delay(0);

                    l.attr("d", linePath)
                    f.attr("d", fillPath)
                    if (activeMax >= masterMax - range) {
                        tip.transition().delay(0);
                        tip.attr("cy", Y(tipData[1]));
                        whiteTip();
                    }
                }

            }

            return {
                redraw: redraw
            };
        }

        function PointSeries(sid, host) {
            var pointGroup = layers.series.append("svg:g")
                                .attr("class", "moki-point-series-" + sid),
                highlight = pointGroup.append("svg:rect").attr("class", "moki-point-highlight"),
                topline = pointGroup.append("svg:line").attr("class", "moki-point-topline"),
                stems = pointGroup.append("svg:g").attr("class", "moki-point-label-stems"),
                dc = $('#detail_content'),
                activeGroup = {},
                defaultColorRange = d3.interpolateHsl("#646773", "#363740"),
                activeColorRange = d3.interpolateHsl("#ff7607", "#cc1414"),
                hitboxes = {},
                currentRedrawHasActive = false;

            $('#detail_backlink').live('click', function () {
                jumpTo(activeGroup.id - range/2);
            });

            function data() {
                var d = series[sid].activeData,
                    i;
                if (host != undefined) {
                    var linkedD = [],
                        hostD = series[host].activeData;
                    for (i = 0; i < d.length; i++) {
                        var x = d[i][0],
                            hostIndex = d3.bisectLeft(hostD, [x, undefined]),
                            y = hostD[hostIndex][1];
                        linkedD.push([x, y, d[i][1]]);
                    }
                    if (!activeGroup.id && linkedD.length) {
                        var datum = linkedD[linkedD.length - 1];
                        setActiveGroup(datum); 
                        showDetail(datum);
                    }
                    return linkedD;
                } else {
                    return d;
                }
            }

            function getRadius(d) {
                var change = Math.round((d[2].end_value - d[2].start_value) / d[2].start_value * 100);
                return Math.round(Math.log(change)) + 1;
            }

            function defaultColor(r) {
                return function (d) {
                    var c = defaultColorRange(1 - (r - d) / 7),
                        rgb = d3.rgb(c);
                    return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ", 1)";
                };
            }

            function toDefaultColor(d) {
                var r = getRadius(d),
                    circles = d3.select(this).selectAll("circle.moki-circle-layer");
                circles.attr("fill", defaultColor(r));
            }

            function activeColor(r) {
                return function (d) {
                    var c = activeColorRange(1 - (r - d) / 7),
                        rgb = d3.rgb(c);
                    return "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ", 1)";
                };
            }

            function defaultSize(d) { return 2 * d; }
            function largerSize(d) { return 2 * d + 2; }

            function toDefaultSize(d) {
                var r = getRadius(d),
                    circles = d3.select(this).selectAll("circle");
                circles.attr("r", defaultSize);
            }

            function toDefaultText(d) {
                d3.select(this).selectAll("text").attr("fill", "#222");
            }

            function positionText(t, r, x, y) {
                var bbox = t.node().getBBox(),
                    yO = -1 * (2*r + 10),
                    xShift = -1 * bbox.width / 2,
                    hitbox = {
                        x1: x + xShift,
                        x2: x - xShift,
                        y1: y + yO - bbox.height / 2,
                        y2: y + yO + bbox.height / 2
                    },
                    key,
                    validPosition = false,
                    offset = 0,
                    increment = 20,
                    dFromEdge = X(masterMax + future) - x;

                // shift hitbox left if on the bleeding edge
                if (dFromEdge < bbox.width / 2) {
                    hitbox.x2 = X(masterMax + future) - 20;
                    hitbox.x1 = hitbox.x2 - bbox.width;
                    xShift = hitbox.x1 - x;
                }

                delete hitboxes[x];
                // prevent labels on hotspots off-screen from intersecting x-axis labels
                if (hitbox.y1 < 30) {
                    t.attr("transform", "translate(" + xShift + ", " + (-1 * 100) + ")");
                    return;
                }

                while (!validPosition) {
                    validPosition = true;
                    for (key in hitboxes) {
                        if (rectangleCollision(hitbox, hitboxes[key])) {
                            validPosition = false;
                            hitbox.y1 -= increment;
                            hitbox.y2 -= increment;
                            offset += increment;
                            break;
                        }
                    }
                }
                hitboxes[x] = hitbox;
                
                t.attr("transform", "translate(" + xShift + ", " + (hitbox.y1 - y + bbox.height/2) + ")");
                if (offset > 0) {
                    var stem = stems.selectAll("line.stem-" + X.invert(x))
                    if (!stem.node()) {
                        stem = stems.append("svg:line").attr("class", "stem-" + X.invert(x)); 
                    }
                    stem
                        .attr("transform", "translate(" + (Math.round(x + 0.5) - 0.5)  + ", " + y + ")")
                        .attr("x1", 0).attr("x2", 0)
                        .attr("y1", yO + 5 - offset).attr("y2", yO + 5)
                        .attr("stroke-width", 1)
                        .attr("stroke", "rgba(0, 0, 0, 0.3)");
                } else {
                    stems.selectAll("line.stem-" + X.invert(x)).remove();
                }
            }

            function setActiveGroup(datum) {
                currentRedrawHasActive = true;
                activeGroup = {
                    id: datum[0],
                    start: datum[2].start_time,
                    end: datum[2].end_time
                };
            }

            function circleStyle(d) {
                var r = getRadius(d),
                    s = d3.select(this),
                    circles = s.selectAll("circle.moki-circle-layer")
                        .data(d3.range(r, 1, -1)),
                    circlesE,
                    sizeFn, colorFn, textFill, textWeight,
                    text, textBBox, textHitbox,
                    isReplacement = d[2].replaced && d[2].replaced.indexOf(activeGroup.id) >= 0;

                if (s.classed("cg-" + activeGroup.id) || isReplacement) {
                    sizeFn = largerSize;
                    colorFn = activeColor(r);
                    textFill = "#222";
                    textWeight = "bold";
                    setActiveGroup(d);
                    showDetail(d);
                } else {
                    sizeFn = defaultSize;
                    colorFn = defaultColor(r);
                    textFill = "#444";
                    textWeight = "normal";
                }

                circles.attr('r', sizeFn);
                circles.attr('fill', colorFn);

                circlesE = circles.enter().insert("svg:circle", ".moki-pin")
                    .attr("class", "moki-circle-layer")
                    .attr("cx", 0)
                    .attr("cy", 0)
                    .attr("r", sizeFn)
                    .attr("fill", colorFn);

                s.selectAll(".moki-pin")
                    .data([1])
                    .attr("r", sizeFn)
                    .enter().append("svg:circle")
                        .attr("class", "moki-pin")
                        .attr("fill", "#fff")
                        .attr("r", sizeFn);
                s.selectAll(".moki-pin-cover")
                    .data([r])
                    .attr("r", sizeFn)
                    .enter().append("svg:circle")
                        .attr("class", "moki-pin-cover")
                        .attr("fill", "transparent")
                        .attr("r", sizeFn);
                if (d[2].summary) {
                    var textSize = W < 800 ? 11 : 12;
                    text = s.selectAll(".moki-circle-label");
                    if (text.node() == null) {
                        text = s.append("svg:text").classed("moki-circle-label", 1);
                    }
                    text
                        .attr("transform", "translate(0, " + (-10 - 2*r) + ")")
                        .attr("fill", textFill)
                        .style("font-size", textSize)
                        .style("font-weight", textWeight)
                        .text(shortenText(d[2].summary, 30))
                        .classed("moki-circle-label", 1);
                    positionText(text, r, X(d[0]), Y(d[1]));
                }
            }

            function circleHover($this) {
                var node = $this.node(),
                    nData = node.__data__,
                    start = nData[2].start_time,
                    end = nData[2].end_time,
                    change = Math.round((nData[2].end_value - nData[2].start_value) / nData[2].start_value * 100),
                    radius = Math.round(Math.log(change)) + 1;
                
                if (!$this.classed("cg-" + activeGroup.id)) {
                    $this.selectAll("circle")
                        .attr("r", function (d) { return 2 * d + 1; });
                    $this.selectAll("text")
                        .attr("fill", "#222");
                }
            }

            function circleActive($this) {
                var node = $this.node(),
                    nData = node.__data__,
                    start = nData[2].start_time,
                    end = nData[2].end_time,
                    r = getRadius(nData),
                    oldActiveGroup = activeGroup,
                    text = $this.selectAll("text");
                setActiveGroup(nData);
                
                d3.selectAll("g.cg-" + oldActiveGroup.id)
                    .each(circleStyle);

                $this.selectAll("circle")
                    .attr("r", largerSize);

                $this.selectAll("circle.moki-circle-layer")
                    .attr("fill", activeColor(r));
                
                if (nData[2].summary) {
                    text
                        .attr("fill", "#222")
                        .style("font-weight", "bold");
                    positionText(text, r, X(nData[0]), Y(nData[1]));
                }

                redrawHighlight();
                jumpToIndicator();
            };

            function redrawHighlight() {
                highlight
                    .style("display", "block")
                    .attr("transform", "translate(0, 0)")
                    .attr("x", X(activeGroup.start))
                    .attr("y", 0)
                    .attr("height", H)
                    .attr("width", X(activeGroup.end) - X(activeGroup.start))
                    .attr("fill", "rgba(43, 151, 164, 0.3)");
                topline
                    .style("display", "block")
                    .attr("transform", "translate(0, 0)")
                    .attr("x1", X(activeGroup.start))
                    .attr("x2", X(activeMax + range))
                    .attr("y1", 1.5)
                    .attr("y2", 1.5)
                    .attr("stroke-width", 3)
                    .attr("stroke", "#d24544");
            }

            function hideHighlight() {
                highlight.style("display", "none");
                topline.style("display", "none");
            }

            function redraw(transition) {
                var circleGroups = pointGroup.selectAll("g.moki-circle-group")
                        .data(data(), function (d) { return d[0]; }),
                    circleGroupsE;
                if (transition == undefined) {
                    transition = true;
                }

                currentRedrawHasActive = true;

                hitboxes = {};
                stems.selectAll("line").remove();

                if (yMax1 != null && nYMax1 != nYMax2 && transition) {
                    circleGroups
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y2(d[1]) + ")"; })
                      .transition().ease("quad").duration(500)
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; })
                      .each("end", circleStyle);
                } else {
                    circleGroups.transition().delay(0);
                    circleGroups
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; })
                      .each(circleStyle);
                }


                circleGroupsE = circleGroups.enter()
                    .append("svg:g")
                        .attr('class', function (d) { return 'cg-' + d[0]; })
                        .classed('moki-circle-group', 1)
                        .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; });

                circleGroupsE.each(circleStyle)
                    .on('mouseover', function (d) {
                        d3.select(this)
                          .call(circleHover)
                          .style("cursor", "pointer");
                    })
                    .on('mouseout', function (d) {
                        var s = d3.select(this);
                        if (!s.classed("cg-" + activeGroup.id)) {
                            s
                              .each(toDefaultSize)
                              .each(toDefaultText)
                              .style("cursor", "default");
                        }
                    })
                    .on('click', function (d) {
                        var s = d3.select(this);
                        if (!s.classed("cg-" + activeGroup.id)) {
                            s
                              .call(circleActive)
                              .each(showDetail);
                        }
                        Mixpanel.track('Hotspot CLICK');
                    });

                circleGroups.exit().remove();

                if (activeGroup.id) {
                    redrawHighlight();
                    jumpToIndicator();
                }
                if (!currentRedrawHasActive) {
                    hideHighlight();
                    hideDetail();
                }
            }

            function showDetail(datum) {
                var html = '',
                    d = datum[2],
                    change = Math.round((d.end_value - d.start_value) / d.start_value * 100),
                    duration = formatDuration(datum[0] - d.start_time),
                    unit = ' / hr';

                html += '<div class="detail_stats">';
                html += '<p>Peak: <strong class="positive">' + formatShortNum(d.end_value) + '</strong> tweets / hr';
                html += ' &nbsp;<span class="tiny">(<strong class="positive">+';
                html += change + '%</strong> over <strong class="dark">' + duration + "</strong>)</span></p>";
                if (d.summary) {
                    html += '<p class="mt_3">Topic: <span class="dark">"' + d.summary + '"</span></p>';
                }
                html += '<p class="mt_3 faded">' + relativeDaysQ(datum[0] * 1000);
                html += ' at ' + moment(datum[0] * 1000).format("h:mma");
                html += '</p>';
                html += '</div><div style="width: 285px;margin:5px 7px;">';
                $.each(d.entities.slice(0, 5), function (i, e) {
                    if (!e) {
                        return;
                    }
                    var dict = e[1];
                    if (i > 0) {
                        html += "<hr />";
                    }
                    // a tweet
                    if (e[0] == 0) {
                        html += tweetUserImage(dict);
                        html += '<div class="right" style="width: 238px;">';
                        html += '<p class="medium strong">' + tweetUserLink(dict) + ' &nbsp;' + tweetLink(dict);
                        html += '</p>';
                        html += '<p class="small mt_2">' + linkify(dict.body) + '</p>';
                        if (dict.retweets != 0 && dict.retweets != '0' && dict.retweets != undefined) {
                            html += '<p class="tiny mt_3 faded"><img src="/images/icons/rt.png" /> <span class="weak faint tiny">' + dict.retweets + ' retweets</span></p>';
                        }
                        html += '</div><div style="clear: both;"></div>';
                    // an article
                    } else {
                        html += '<p class="strong small"><span style="color: #d92b42;">HEADLINE: </span><a href="' + dict.link + '" target="_blank" class="headline_link">' + dict.title;
                        html += '</a> <span class="faded weak">(' + dict.domain + ')</span></p>';
                    }
                });
                html += '</div>';

                dc.html(html);
            }

            function hideDetail() {
                dc.html('<p class="small faded" style="margin: 5px 7px;">No hotspot selected.</p>');
            }

            function jumpToIndicator() {
                if (!inRange(activeMin, activeGroup.id, activeMax)) {
                    $('#detail_backlink').show();
                } else {
                    $('#detail_backlink').hide();
                }
            }

            return {
                redraw: redraw,
                setActiveGroup: setActiveGroup,
                showDetail: showDetail
            };            
        }

        function PointSeries2(sid, host) {
            var pointGroup = layers.series.append("svg:g")
                .attr("class", "moki-point-series-" + sid),
                highlight = pointGroup.append("svg:rect").attr("class", "moki-point-highlight");

            function data() {
                var d = series[sid].activeData,
                    i;
                if (host != undefined) {
                    var linkedD = [],
                        hostD = series[host].activeData;
                    for (i = 0; i < d.length; i++) {
                        var x = d[i][0],
                            hostIndex = d3.bisectLeft(hostD, [x, undefined]),
                            y = hostD[hostIndex][1];
                        linkedD.push([x, y, d[i][1]]);
                    }
                    return linkedD;
                } else {
                    return d;
                }
            }
            function redraw() {
                var color = "#000",
                    circleGroups = pointGroup.selectAll("g")
                        .data(data(), function (d) { return d[0]; }),
                    circleGroupsE = circleGroups.enter().append("svg:g")
                        .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; }),
                    circleStyle = function (d) {
                        var radius = 3,
                            circles = d3.select(this).selectAll("circle")
                            .data([radius])
                            .attr("r", function (d) { return 2 * d; });
                        circles.enter().append("svg:circle")
                            .attr("fill", color)
                            .attr("cx", 0)
                            .attr("cy", 0)
                            .attr("r", function (d) { return 2 * d; });
                        d3.select(this).selectAll(".moki-pin")
                            .data([3 / 2])
                            .attr("r", 3)
                            .enter().append("svg:circle")
                                .attr("class", "moki-pin")
                                .attr("fill", "#fff")
                                .attr("r", 3);
                        d3.select(this).selectAll(".moki-pin-cover")
                            .data([radius])
                            .attr("r", radius * 2)
                            .enter().append("svg:circle")
                                .attr("class", "moki-pin-cover")
                                .attr("fill", "transparent")
                                .attr("r", radius * 2);
                    },
                    circleHover = function ($this) {
                        var node = $this.node(),
                            nData = node.__data__,
                            start = nData[2].start_time,
                            end = nData[2].end_time;
                        $this.selectAll("circle")
                            .attr("r", function (d) { return 2 * d + 2; });
                    };

                function showTooltip(datum) {
                    var html = '',
                        hitbox = hitBox(this),
                        $this = d3.select(this),
                        d = datum[2],
                        change = Math.round((d.end_value - d.start_value) / d.start_value * 100),
                        duration = formatDuration(d.end_time - d.start_time),
                        unit = ' / hr';

                    html += '<div id="POI_tooltip_body" class="tooltip">';
                    html += '<div class="tooltip_header clearfix"><div class="left">';
                    html += '<span class="strong">Top Tweet</span>';
                    html += '</div></div>';
                    html += '<div class="tooltip_body">';
                    html += '<div class="conv_tooltip tooltip_inner">';
                    $.each(d.entities.slice(0, 3), function (i, e) {
                        if (!e) {
                            return;
                        }
                        var dict = e[1];
                        if (i > 0) {
                            html += "<hr />";
                        }
                        // a tweet
                        if (e[0] == 0) {
                            html += tweetUserImage(dict);
                            html += '<div class="right" style="width: 294px;">';
                            html += '<p class="medium strong">' + tweetUserLink(dict) + ' &nbsp;' + tweetLink(dict);
                            if (dict.retweets != 0 && dict.retweets != '0' && dict.retweets != undefined) {
                                html += ' &nbsp;<img src="/images/icons/rt.png" /> <span class="weak faint tiny">' + dict.retweets + ' retweets</span>';
                            }
                            html += '</p>';
                            html += '<p class="small mt_2">' + linkify(dict.body) + '</p>';
                            html += '<p class="tiny mt_3 faded">(1 of ' + (dict.number != undefined ? dict.number : 1) + ' related conversations)</p>';
                            html += '</div><div style="clear: both;"></div>';
                        // an article
                        } else {
                            html += '<p class="strong small"><span style="color: #d92b42;">HEADLINE: </span><a href="' + dict.link + '" target="_blank" class="headline_link">' + dict.title;
                            html += '</a> <span class="faded weak">(' + dict.domain + ')</span></p>';
                            if (dict.number != undefined) {
                                html += '<p><span class="tiny faded weak">(' + dict.number + ' related conversations)</span></p>';
                            }
                        }
                    });
                    html += '</div>';
                    html += '</div>';
                    html += '</div>';
                    
                    tooltip.setContent(hitbox.xc, hitbox.yc, html).addHandler(function () {
                        $this.each(circleStyle)
                          .style("cursor", "default");
                        highlight.style("display", "none");
                    }).show($(this));
                }

                if (yMax1 != null) {
                    circleGroups
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y2(d[1]) + ")"; })
                      .transition().ease("quad").duration(500)
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; });
                } else {
                    circleGroups
                      .attr("transform", function (d) { return "translate(" + X(d[0]) + "," + Y(d[1]) + ")"; });
                }
                circleGroups.each(circleStyle);
                circleGroupsE.each(circleStyle)
                    .on('mouseover', function (d) {
                        d3.select(this)
                          .call(circleHover)
                          .each(showTooltip)
                          .style("cursor", "pointer");
                    });
                circleGroups.exit().remove();
            }
            return {
                redraw: redraw
            };            
        }

        function rescale() {
            setYScale(yMax2);
            setY2Scale(yMax1);
            setXScale();
        }

        function setYScale(y) {
            Y = d3.scale.linear().domain([y, 0]).range([65, H]);
            Y.nice();
            nYMax1 = Y.invert(65);
        } 

        function setY2Scale(y) {
            Y2 = d3.scale.linear().domain([y, 0]).range([65, H]);
            Y2.nice();
            nYMax2 = Y2.invert(65);
        }

        function setXScale() {
            X = d3.scale.linear().domain([activeMin, activeMax]).range([0, W]);
        }

        function Legend() {
            var legendSVG = d3.select("#legend").append("svg:svg")
                              .attr("height", 60),
                legend = legendSVG.append("svg:g").attr("class", "moki-legend"),
                inner = legend.append("svg:g").attr("class", "moki-legend-inner"),
                yT = function () { return -1 * H + 5; };

            var line = inner.append("svg:g").attr("transform", "translate(10, 50)");
            line.selectAll("path").data([[[0, 1], [0, 3], [0, 2], [0, 5], [0, 4], [0, 1], [0, 3], [0, 4]]])
                .enter().append("svg:path").attr("d", d3.svg.line()
                    .x(function (d, i) { return i * 7; })
                    .y(function (d) { return d[1] * -2; }))
                    .attr("fill", "none")
                    .attr("stroke", "#2B97A4")
                    .attr("stroke-width", 2);
            line.append("svg:text")
                .attr("fill", "#2B97A4")
                .attr("x", 60)
                .attr("y", -1)
                .style("font-size", "11px")
                .text("# of tweets / hour");

            var point = inner.append("svg:g").attr("transform", "translate(10, 25)"),
                circleGroups = point.selectAll("g")
                        .data([[1.5, 5.5, 4], [5, 8, 6], [9, 5, 5]])
                        .enter().append("svg:g")
                        .attr("transform", function (d) { return "translate(" + (d[0] * 5) + "," + (-1 * d[1]) + ")"; }),
                color = d3.interpolateHsl("#ff5307", "#cc1414"),
                color = d3.interpolateHsl("#646773", "#363740"),
                circleStyle = function (d) {
                    var circles = d3.select(this).selectAll("circle")
                        .data(function (d) { return d3.range(d[2], 1, -1); })
                        .attr("r", function (d) { return d; });
                    circles.enter().append("svg:circle")
                        .attr("fill", function (d2) {
                            var iColor = color(1 - (d[2] - d2) / 4),
                                iRGB = d3.rgb(iColor);
                            return "rgba(" + iRGB.r + "," + iRGB.g + "," + iRGB.b + ", 1)";
                        })
                        .attr("cx", 0)
                        .attr("cy", 0)
                        .attr("r", function (d) { return d; });
                    d3.select(this).selectAll(".moki-pin").data([0])
                        .enter().append("svg:circle")
                            .attr("fill", "#fff")
                            .attr("r", 1);
                };
            circleGroups.each(circleStyle);
            point.append("svg:text")
                .attr("fill", "#363740")
                .attr("x", 60)
                .attr("y", -1)
                .style("font-size", "11px")
                .text("Hotspots");
        }

        function niceTicks(max, count) {
            var ticks = [],
                scale = Math.ceil(Math.log(max) / Math.LN10),
                unit = Math.pow(10, (scale - 1)) / 2,
                numUnits = Math.floor(max / unit),
                step = Math.floor(numUnits / (count - 1)),
                i;
            for (i = 0; i < count - 1; i++) {
                ticks.push(max - i * step * unit);
            }
            return ticks;
        }

        function xAxis() {
            var axisLayers = {
                    bg: layers.xaxis.append("svg:g").attr("class", "moki-x-bg"),
                    fg: layers.xaxis.append("svg:g").attr("class", "moki-x-fg"),
                    hl: layers.xaxis.append("svg:g").attr("class", "moki-x-hl")
                };

            function redraw() {
                var minDate = new Date(extendedMin() * 1000),
                    maxDate = new Date(extendedMax() * 1000),
                    interval = W < 800 ? 12 : 6,
                    hours = d3.time.hours(
                                minDate,
                                maxDate,
                                interval),
                    days = d3.time.days(
                                new Date(extendedMin(2) * 1000),
                                maxDate,
                                1),
                    dayTicks = axisLayers.bg.selectAll(".moki-chart-x-day").data(days)
                                .attr("transform", function (d) { return "translate(" + (Math.floor(X(d.getTime() / 1000)) + 0.5) + ", 0)";}),
                    dayTicksE = dayTicks.enter().append("svg:g")
                                .attr("transform", function (d) { return "translate(" + (Math.floor(X(d.getTime() / 1000)) + 0.5) + ", 0)"; })
                                .attr("class", "moki-chart-x-day"),
                    ticks = axisLayers.fg.selectAll(".moki-chart-x-tick").data(hours)
                                .attr("transform", function (d) { return "translate(" + (Math.floor(X(d.getTime() / 1000)) + 0.5) + ", 0)"; }),
                    ticksE = ticks.enter().append("svg:g")
                                .attr("transform", function (d) { return "translate(" + (Math.floor(X(d.getTime() / 1000)) + 0.5) + ", 0)"; })
                                .attr("class", "moki-chart-x-tick"),
                    formatXDate = function (d) {
                        if (d.getHours() == 0) {                  
                            return moment(d).format("ddd M/D");
                        } else {
                            return moment(d).format("ha");
                            var h = (d.getHours() % 12) || 12;
                            return h + d3.time.format("%p")(d).toLowerCase();
                        }
                    },
                    lineStyle = function (d) {
                        if (d.getHours() == 0) {
                            d3.select(this)
                              .attr("stroke-width", 1)
                              .attr("stroke", "#aaa")
                              .attr("stroke-dasharray", "none")
                              .attr("y1", 0)
                              .attr("y2", H)
                              .attr("x1", 0);
                        } else {
                            d3.select(this)
                              .attr("stroke-width", 1)
                              .attr("stroke", "#ccc")
                              .attr("stroke-dasharray", "2")
                              .attr("y1", 0)
                              .attr("y2", H)
                              .attr("x1", 0);
                        }
                    },
                    textStyle = function (d) {
                        var tSize, yOff;
                        if (W < 800) {
                            tSize = 11;
                            yOff = 13;
                        } else {
                            tSize = 13;
                            yOff = 15;
                        }
                        if (d.getHours() == 0) {
                            d3.select(this)
                              .attr("dy", yOff)
                              .attr("dx", 4)
                              .attr("fill", "#888")
                              .style("font-size", tSize)
                              .style("font-weight", "bold")
                              .text(formatXDate);
                        } else {
                            d3.select(this)
                              .attr("dy", 13)
                              .attr("dx", 4)
                              .attr("fill", "#888")
                              .style("font-size", "11px")
                              .style("font-weight", "normal")
                              .text(formatXDate);
                        }
                    },
                    rectStyle = function (d, i) {
                        var color;
                        if (Math.floor(((new Date()).getTime() - d.getTime()) / 864E5) % 2) {
                            if (Math.floor(d.getHours() / 3) % 2) {
                                color = "#f0f2f5";
                            } else {
                                color = "#edeff2";
                                color = "#e8eaed";
                            }
                        } else {
                            if (Math.floor(d.getHours() / 3) % 2) {
                                color = "#f4f0f5";
                            } else {
                                color = "#f1edf2";
                                color = "#f0f2f5";
                            }
                        }
                        d3.select(this)
                          .attr("width", W)
                          .attr("height", H)
                          .attr("fill", color);
                    };

                dayTicks.select("rect")
                    .each(rectStyle);
                dayTicksE.append("svg:rect")
                    .each(rectStyle);

                ticks.select("text")
                    .each(textStyle);
                ticks.select("line")
                    .each(lineStyle);

                ticksE.append("svg:text")
                    .each(textStyle);

                ticksE.append("svg:line")
                    .each(lineStyle);


                ticks
                    .exit().remove();
                dayTicks
                    .exit().remove();
            }
            return {
                redraw: redraw,
                layers: axisLayers
            };
        }

        function yAxis(transition) {
            var axisLayers = {
                    bg: layers.yaxis.append("svg:g").attr("class", "moki-y-bg"),
                    fg: layers.yaxis.append("svg:g").attr("class", "moki-y-fg"),
                    hl: layers.yaxis.append("svg:g").attr("class", "moki-y-hl")
                };

            function slideX(offset) {
                layers.yaxis.attr("transform", "translate(" + (-1 * offset) + ", 0)");
            }

            function redraw(transition) {
                var tickData = niceTicks(Y.invert(65), 3),
                    ticks = axisLayers.bg.selectAll(".moki-y-tick").data(tickData),
                    ticksE = ticks.enter().append("svg:g")
                                .attr("transform", function (d) {
                                    this.__t__ = Math.floor(Y(d)) + 0.5;
                                    return "translate(0, " + this.__t__ + ")";
                                })
                                .attr("class", "moki-y-tick"),
                    textStyle = function (d, i) {
                        var $this = d3.select(this),
                            innerG = $this.select("g"),
                            text = $this.select("text"),
                            rect,
                            bbox,
                            textFill;
                        if (innerG.empty()) {
                            innerG = $this.append("svg:g");
                        }
                        if (text.empty()) {
                            text = innerG.append("svg:text");
                        }
                        if (i == 0) {
                            textFill = "#444";
                        } else {
                            textFill = "#888";
                        }
                        text
                          .attr("dy", 13)
                          .attr("dx", 4)
                          .attr("fill", textFill)
                          .style("font-size", "11px")
                          .text(function (d) {
                              if (d) {
                                  return addCommasToNum(String(d)) + " tweets / hr";
                              }
                              return "No data in this time period.";
                          });
                        bbox = text.node().getBBox();
                        this.__bbox__ = bbox;

                        rect = innerG.selectAll("rect")
                            .data([bbox, bbox]);
                        rect
                            .attr("x", function (d, i) { return d.x - 6 - i; })
                            .attr("y", function (d, i) { return d.y - 1 - i; })
                            .attr("height", function (d, i) { return d.height + 4.5; })
                            .attr("width", function (d, i) { return d.width + 12; });
                        rect
                            .enter().insert("svg:rect", "text")
                            .attr("x", function (d, i) { return d.x - 6 - i; })
                            .attr("y", function (d, i) { return d.y - 1 - i; })
                            .attr("width", function (d, i) { return d.width + 12; })
                            .attr("height", function (d, i) { return d.height + 4.5; })
                            .style("fill", function (d, i) {
                                if (i) {
                                    return "#fff";
                                } else {
                                    return "rgba(0, 0, 0, 0.3)";
                                }
                            });
                        this.__hitbox__ = hitBox(this);
                    };
                if (transition == undefined) {
                    transition = true;
                }

                if (yMax1 != null && nYMax1 != nYMax2 && transition) {
                    ticks
                      .attr("transform", function (d) {
                          return "translate(0, " + (Math.floor(Y2(d)) + 0.5) + ")";
                      })
                      .transition()
                      .attr("transform", function (d) {
                          this.__t__ = Math.floor(Y(d)) + 0.5;
                          return "translate(0 , " + this.__t__ + ")";
                      });
                } else {
                    ticks
                      .attr("transform", function (d) {
                          this.__t__ = Math.floor(Y(d)) + 0.5;
                          return "translate(0 , " + this.__t__ + ")";
                      });
                }

                ticks.each(textStyle);

                ticksE.each(textStyle)
                  .on("mouseover", function (d) {
                      var $this = d3.select(this),
                          hitbox = hitBox(this);
                      bindHitbox("leave", hitbox, function () {
                          $this
                            .transition().duration(200).delay(500)
                            .attr("transform", "translate(0, " + $this.node().__t__ + ")");
                      });
                      $this
                          .transition().duration(200)
                          .attr("transform", "translate(" + (8 - hitbox.width) + ", " + this.__t__ + ")");
                  });

                ticks
                    .exit().remove();
            }
            return {
                redraw: redraw,
                slideX: slideX,
                layers: axisLayers
            };
        }

        function get2DExtremes(coordinateList) {
            var i, xm, xM, ym, yM;
            for (i = 0; i < coordinateList.length; i++) {
                var x = coordinateList[i][0],
                    y = coordinateList[i][1];
                if (x < xm || xm === undefined) {
                    xm = x;
                }
                if (x > xM || xM === undefined) {
                    xM = x;
                }
                if (y < ym || ym === undefined) {
                    ym = y;
                }
                if (y > yM || yM === undefined) {
                    yM = y;
                }
            }
            return {
              x: {m: xm, M: xM},
              y: {m: ym, M: yM}
            }
        }

        function subscribe(dataObject) {
            var callback = function (sid, points) {
                update(sid, points);
            };
            dataObject.setCallback(callback);
        }

        function Top(container) {
            
            var container = $(container),
                masterData = [],
                masterDict = {};

            container.find(".notable_row").live('click', function () {
                var id = +$(this).attr("id"),
                    datum = [id, null, masterDict[id]];
                series[2].series.setActiveGroup(datum);
                series[2].series.showDetail(datum);
                jumpTo(id - range/2);
                Mixpanel.track('Notable CLICK');
            });

            function update() {
                var data = $.extend(true, [], series[2].masterData), // deep copy
                    filteredData = data.filter(function (x) {
                        return (new Date()).getTime() / 1000 - range / 2 > x[0];
                    }),
                    sortedData = filteredData.sort(function (a, b) {
                        return b[1].end_value - a[1].end_value;
                    });
                masterData = sortedData.slice(0, 5);
                draw();
            }

            function draw() {
                var i;
                container.html('');
                for (i = 0; i < masterData.length; i++) {
                    container.append(notableHTML(masterData[i]));
                    masterDict[masterData[i][0]] = masterData[i][1];
                }
            }

            function notableHTML(point) {
                var id = point[0],
                    html = '';
                html += '<div id="' + id + '" class="notable_row clearfix">';
                html += '<div class="left notable_icon ml_2 mr_7"></div>';
                html += '<div class="left" style="width: 150px;">';
                html += '<p class="positive"><strong>' + formatShortNum(point[1].end_value) + '</strong> <span class="faded">tweets / hr</span></p>';
                html += '<p>' + point[1].summary.substring(0, 50) + (point[1].summary.length > 50 ? '...' : '') + '</p>';
                html += '<p class="faded">' + relativeDays(point[0] * 1000) + '</p>';
                html += '</div>';
                html += '</div>';
                return html;
            }

            function subscribe(dataObject) {
                var callback = function (points) {
                    update(points);
                };
                dataObject.setCallback(callback);
            }
            
            return {
                update: update,
                masterData: masterData,
                subscribe: subscribe
            };
        }

        return {
            update: update,
            clear: clear,
            series: series, 
            subscribe: subscribe,
            Top: Top,
            setHash: setHash
        }

    }

    function TimelineData() {

        var callback;

        function update(sid, points) {
            this.callback(sid, points);
            return this;
        }

        function setCallback(fn) {
            this.callback = fn;
        }

        function removeCallback(fn) {
            this.callback = function () {};
        }

        return {
            update: update,
            setCallback: setCallback,
            removeCallback: removeCallback
        };
    }

    function TopData() {

        var callback;

        function update(points) {
            this.callback(points);
            return this;
        }

        function setCallback(fn) {
            this.callback = fn;
        }

        function removeCallback(fn) {
            this.callback = function () {};
        }

        return {
            update: update,
            setCallback: setCallback,
            removeCallback: removeCallback
        };
    }

    function Tooltip() {
        
        var t = $('#POI_tooltip'),
            X, Y, L, T,
            W = 367,
            activeTarget,
            handlers = [];

        function init() {
            bindHide(t);
        }
        init();

        function bindHide(element) {
            element.mouseleave(function (e) {
                var into = $(e.relatedTarget);
                if (!into.closest('#POI_tooltip').length) {
                    t.hide();
                    clearHandlers();
                    if (activeTarget) {
                        activeTarget.unbind('mouseleave');
                        activeTarget.removeClass('active');
                    }
                }
            });
        }

        function setOrigin(x, y, width, height, orientation) {
            X = x;
            Y = y;
            if (orientation == "top") {
                T = Y - 10 - height;
                L = Math.min($(window).width() - width, Math.max(X - width / 2, 0));
                var xOffset = X - L;
                setPosition(L, T);
                $("#tooltip_spacer").addClass("top");
                $("#tooltip_spacer").css("left", xOffset - 20);
            } else {
                var yOffset = Math.max(Y - 20 + height - $(window).height(), 0);
                T = Math.min(Y - 20, $(window).height() - height);
                if (overflowX()) {
                    L = X - width - 5;
                    $("#tooltip_spacer").addClass("left");
                } else {
                    L = X + 5;
                    $("#tooltip_spacer").addClass("right");
                }
                setPosition(L, T);
                $("#tooltip_spacer").css('top', yOffset);
            }
            return this;
        }

        function setContent(x, y, html, orientation) {
            t.html(html);
            t.append('<div id="tooltip_spacer"><span></span></div>');
            var inner = t.find('.tooltip'),
                props = {position: "absolute", visibility: "hidden", display: "block" },
                height;
            $.swap(t[0], props, function () {
                height = inner.height();
                setOrigin(x, y, W, height, orientation);
            });
            return this;
        }

        function setPosition(left, top) {
            t.css({
              left: left,
              top: top
            });
            return this;
        }
        
        function setWidth(w) {
            W = w;
            return this;
        }

        function addHandler(f, arg) {
            handlers.push([f, arg]);
            return this;
        }

        function clearHandlers() {
            $.each(handlers, function (i, handler) {
                handler[0](handler[1]);
            });
            handlers = [];
        }

        function overflowX() {
            return X + W + 5 > 788;
        }

        function overflowY() {
            return Y
        }

        function show(target) {
            t.show();

            activeTarget = target;
            activeTarget.addClass('active');
            bindHide(activeTarget);

            var inner = t.find('.tooltip');
            t.css({
                height: inner.height(),
                width: inner.width()
            });
            return this;
        }

        return {
            setOrigin: setOrigin,
            setContent: setContent,
            addHandler: addHandler,
            show: show
        };
    }
        
    window.Moki = {
        Init: Init,
        TimelineData: TimelineData,
        TopData: TopData
    };

}($));

