/*
    WebStats Scripts
 
    Copyright(c) 2008, Jeff Schiller (http://blog.codedread.com/).
    
    Several enhancements provided by Johan Sundstrom (http://ecmanaut.blogspot.com/)
    and the full article about how this came to be is available at
    http://ecmanaut.blogspot.com/2006/01/svg-challenge-taken.html
    
    BIZARRE NOTE: ASV's scripting engine does not like the way Johan's last name is 
    spelled so I have modified it.

    Permission is hereby granted to use and/or modify this software as long as 
    credits are given to the creators listed above.

    Reference: http://www.rad-e8.com/log/i/0912diag_ua.jpg

    Enhancements since the last iteration:
    
    - Added ability to filter out particular browsers
    - Grouped legacy browsers under a single category 
    - Added all of Johan's enhancements (including the CSS, the better text, more labels)
    - Made the legend draggable (bug: in IE after you drag, it requires 2 clicks to move the horiz cursor)
    - Fixed colour scheme for all browsers
    - Made the horizontal cursor draggable, pie chart updates as you drag (slowly at this time)
    - Moved time-based chart drawing to client side
    - Made grippies part of a scrollbar that allows panning (slick but slow - much faster in Opera 9.5 Alpha!)
*/

var ns = {  
            svg:'http://www.w3.org/2000/svg', 
            xlink:'http://www.w3.org/1999/xlink',
            drag:'http://www.codedread.com/dragsvg'
         };
var KEY_LEFT = 37;
var KEY_RIGHT = 39;
// TO DO: KEY_UP and DOWN here

var css = {
    // My Colours
    // Scheme is:  The most recent browser version matches the primary colour
    //             of the given browser's logo.  Earlier versions get lighter
    //             as they go back in time.  Pre-Release version gets darker.
    //             "Other" is the darkest.
    //
    // These styles below should be in sync with 
    
    // Oranges
    FirefoxOther:   { fill: 'rgb(232,100,0)' },
    Firefox31x:     { fill: 'rgb(244,120,0)' }, 
    Firefox30x:     { fill: 'rgb(255,140,20)' }, 
    Firefox20x:     { fill: 'rgb(255,160,50)' }, 
    Firefox15:      { fill: 'rgb(255,180,85)' },

    // Cyans
    SafariOther:    { fill: 'rgb(0,160,240)' }, 
    Safari40x:      { fill: 'rgb(20,170,241)' },
    Safari31x:      { fill: 'rgb(50,190,243)' },
    Safari30x:      { fill: 'rgb(90,202,245)' },
    Safari20:       { fill: 'rgb(150,224,255)' }, 
    
    // Reds
    OperaOther:     { fill: 'rgb(158,20,23)' },
    Opera95x:       { fill: 'rgb(178,23,25)' },
    Opera90x:       { fill: 'rgb(205,45,48)' },
    Opera85:        { fill: 'rgb(232,68,72)' },

    // Canary yellow, like their logo
    Konqueror:      { fill: 'rgb(255,243,0)' }, 
    
    // Blues
    IEOther:        { fill: 'rgb(0,60,100)' },
    IE80x:          { fill: 'rgb(0,79,131)' },
    IE70x:          { fill: 'rgb(0,90,151)' },
    IE60x:          { fill: 'rgb(0,115,191)' },
    IE55:           { fill: 'rgb(0,133,221)' },
    
    ChromeOther:     { fill: 'rgb(128,128,128)' },
    Chrome02x:       { fill: 'rgb(160,160,160)' },

    // Other Browsers and OS    
    Other:          { fill: 'rgb(160,160,160)' },

//    // Windows Family
//    Windows2000:        { fill: 'rgb(0,0,255)' },
//    WindowsXP:          { fill: 'rgb(0,0,192)' },
//    Windows2003Server:  { fill: 'rgb(0,0,160)' },
//    WindowsVista:       { fill: 'rgb(0,0,128)' },
//    WindowsOther:       { fill: 'rgb(0,0,80)' },
//
//    // Mac
//    Mac:            { fill: 'rgb(0,160,0)' },
//
//    // Linux
//    Linux:          { fill: 'rgb(160,0,0)' },
//    

    OuterRing:    { fill: '#777', stroke: 'none' },
    InnerRing:    { fill: 'none', stroke: '#FFF', "stroke-opacity": '0.35', "stroke-width": '1.4px' },
    BrVerFill:    { fill: '#FFF', stroke: 'none', "fill-opacity": '0.1' },
    BrPctFill:    { fill: '#FFF', stroke: 'none', "fill-opacity": '0.1' },
    PieTitle:     { fill: '#000', stroke: 'none', "text-anchor": 'middle', "font-size": '16px' },
    PieSubTitle:  { fill: '#000', stroke: 'none', "text-anchor": 'middle', "font-size": '12px' },
    label:        { fill: '#FFF', stroke: 'none', "text-anchor": 'middle', "font-size": '9px', "text-shadow": '#666 1px 1px 1px' },
    browser:      { "font-weight": 'bold', "font-size": '11px', "text-shadow": '#000 1px 1px 1px' },
    wedge:        { stroke: 'none' },
    
    RollingAve:   { fill: 'none', stroke: 'black', "stroke-width": '1', "stroke-dasharray": "3,2" }
};

var gCursorX = NumDataPoints-1;
var bDraggingLegend = false;

// specify the limits of the visible x-axis for the stats
var browserStartPoint = 0;
var browserEndPoint = NumDataPoints-1;
var BrowserCurrentMaxY = 0;
var BrowserEnables = [];
var ptElems = [];
var bDistribution = false;

//var osStartPoint = 0;
//var osEndPoint = NumDataPoints-1;
//var OSCurrentMaxY = 0;

// for some reason, Opera seems not to apply CSS styles to created nodes. :-(
function forceStyles( attr )
{
    var all = attr["id"]+' '+attr["class"], id, type, styles, selector = /\S+/g;
    while( id = selector.exec( all ) )
    {
        if( (styles = css[id[0]]) )
        for( type in styles ) // (attr[type] ? attr[type]+';' : '' ) + 
            attr[type] = styles[type];
    }
  return attr;
}

function $( id )
{
  return document.getElementById( id );
}

function make( name, attr, text, url )
{
    var node = document.createElementNS( ns.svg, name );
    var name = '', atns;
    if(attr) {
        attr = forceStyles( attr );
        for( var i in attr )
        {
            atns = (/^(.*):/.exec( i )||[,null])[1];
            node.setAttributeNS( atns && ns[atns], i, attr[i] );
        }
    }
    if( text )
    {
        text = document.createTextNode( text );
        node.appendChild( text );
        if( url )
        {
            var a = make( 'a', { 'xlink:href':url } );
            a.appendChild( node );
            return a;
        }
    }
    return node;
}

function removeChildren( node )
{
  while( node.hasChildNodes() )
  node.removeChild( node.firstChild );
}

function cssname( name )
{
  return name.replace( /[. /-]/g, '' );
}

function draggingLegend() {
    bDraggingLegend = true;
}

/*
function onMouseOverLegend() {
  $("browser_legend_scaler").setAttributeNS(null, "transform", "scale(1.0,1.0)");
}

function onMouseOutLegend() {
  if(!bDraggingLegend) {
    $("browser_legend_scaler").setAttributeNS(null, "transform", "scale(0.5,0.5)");
  }
}
*/

// SVG file will have the raw data in:
// - NumDataPoints is the number of data points
// - BrowserData maps series names to arrays of values
// - Colors maps series names to RGB color strings
function init()
{
    console.log('webstats init');
    
    document.documentElement.addEventListener("keypress", getKey, false);
    document.documentElement.addEventListener("click", clickGraph, false);
    
    var legend = $("browser_legend");
    if(legend) {
        legend.addEventListener("mousedown", draggingLegend, false);
//            legend.addEventListener("mouseover", onMouseOverLegend, false);
//            legend.addEventListener("mouseout", onMouseOutLegend, false);
    }
    
    // set up a listener for the horizontal cursor being dragged
    addDragEventListener("dragstart", dragStart);
    addDragEventListener("dragmove", dragMove);
    addDragEventListener("dragdrop", dragDrop);

    createTimeBasedChart();
    
    // set up pie chart elements
    BrowserPieElements = new PieElementsContainer();

    drawPieChart();
    drawLegend();
    showLegend();
}

// TODO: KEY_UP and KEY_DOWN changes the currently selected stat
function getKey(evt) {
    switch(evt.keyCode) {
    case KEY_LEFT:
        evt.preventDefault();
        if(gCursorX > browserStartPoint) {
            --gCursorX;
            // update cursor
            var cur = $("browser_horizcurs");
            if(cur) {
                var x = 400*(gCursorX-browserStartPoint)/(browserEndPoint-browserStartPoint);
                cur.setAttributeNS(null, "transform", "translate(" + x + ",0)");
                cur.setAttributeNS(ns.drag, "x", x);
            }

            // update pie chart
            drawPieChart();
        }
        break;
    case KEY_RIGHT:
        evt.preventDefault();
        if(gCursorX < browserEndPoint) {
            ++gCursorX;
            // update cursor
            var cur = $("browser_horizcurs");
            if(cur) {
                var x = 400*(gCursorX-browserStartPoint)/(browserEndPoint-browserStartPoint);
                cur.setAttributeNS(null, "transform", "translate(" + x + ",0)");
                cur.setAttributeNS(ns.drag, "x", x);
            }

            // update pie chart
            drawPieChart();
        }
        break;
    }
}

var bDraggingLatch = false;

function dragMove(evt) {
    if(!bDraggingLatch) {
        bDraggingLatch = true;
        
        if(evt.dragEnt == $('browser_horizcurs')) {
            var newCursorX = browserStartPoint + Math.round( (evt.userx/400) * (browserEndPoint-browserStartPoint) );
            if(newCursorX >= browserStartPoint && newCursorX <= browserEndPoint) {
                gCursorX = newCursorX;
                drawPieChart();
            }
        }
        else if(evt.dragEnt == $('browser_leftgrip') || evt.dragEnt == $('browser_rightgrip')) {
            var left = parseFloat($('browser_leftgrip').getAttributeNS(ns.drag,"x"));
            var right = 400 + parseFloat($('browser_rightgrip').getAttributeNS(ns.drag,"x"));
            changeGlobalXAxisBounds(left,right);
        }
        else if(evt.dragEnt == $('browser_scrollbar') ) {
            var left = parseFloat($('browser_scrollbar').getAttributeNS(ns.drag,"x"));
            var right = left + parseFloat($('browser_scrollbar').getAttributeNS(null, "width"));
            changeGlobalXAxisBounds(left,right);
        }
        else {
            // moving legend - who cares
        }
        
        bDraggingLatch = false;
    }
}

function dragStart(evt) {
    if( evt.dragEnt == $('browser_leftgrip') || 
        evt.dragEnt == $('browser_rightgrip') ||
        evt.dragEnt == $('browser_scrollbar')
        ) 
    {
        // remove axes
        var axes = $('browser_timechartaxes');
        removeChildren(axes);
    }
}

// recreate axes
function dragDrop(evt) {
    if( evt.dragEnt == $('browser_leftgrip') || 
        evt.dragEnt == $('browser_rightgrip') ||
        evt.dragEnt == $('browser_scrollbar')
        ) 
    {
        // this will change the MaxY and the gCursorX
        reflowYAxis();
        reflowPie();

        // this will potentially rescale in the y-direction
        var left = parseFloat($('browser_leftgrip').getAttributeNS(ns.drag,"x"));
        var right = 400 + parseFloat($('browser_rightgrip').getAttributeNS(ns.drag,"x"));
        changeGlobalXAxisBounds(left,right);
    }
}

function getRoot() {
  if (document.documentElement.namespaceURI == ns.svg) {
    return document.documentElement;
  } else if (document.getElementsByTagNameNS(ns.svg, 'svg').length > 0) {
    return document.getElementsByTagNameNS(ns.svg, 'svg')[0];
  } else {
    throw 'No SVG root';
  }
}

function clickGraph(evt) {
    // only process if the click was on the graph (not the legend)
    if(bDraggingLegend) { 
        bDraggingLegend = false;
        return;
    }
    
    var root = getRoot();
        
    var p = root.createSVGPoint();
    console.log(p);
    console.log(evt);
    p.x = evt.clientX;
    p.y = evt.clientY;

    var screenCTMInv = getScreenCTM().inverse();
    p = p.matrixTransform(screenCTMInv);
    
    if(p.x >= 0 && p.y >= 0 && p.x <= 400 && p.y <= 300) {
        // determine the data point to use
        gCursorX = browserStartPoint + Math.round( (p.x/400) * (browserEndPoint-browserStartPoint) );

        // actual X will be aligned with the data points
        var actualX = (400 * (gCursorX-browserStartPoint)) / (browserEndPoint-browserStartPoint);
        
        // update cursor
        var cur = $('browser_horizcurs');
        if(cur) {
            cur.setAttributeNS(null, "transform", "translate(" + actualX + ",0)");
            cur.setAttributeNS(ns.drag, "x", actualX);
        }
        
        // update pie chart
        drawPieChart();
    }
}

function changeGlobalXAxisBounds(left, right) {
    // modify the constraints of each grip
    var leftgrip = $('browser_leftgrip');
    var rightgrip = $('browser_rightgrip');
    var scrollBar = $('browser_scrollbar');

    if(!leftgrip || !rightgrip || !scrollBar) { return; }

    leftgrip.setAttributeNS(null, "transform", "translate(" + left + ")");
    leftgrip.setAttributeNS(ns.drag, "x", left);
    leftgrip.setAttributeNS(ns.drag,"constraint-right",(right - (10 * 400 / NumDataPoints)));
    
    rightgrip.setAttributeNS(null, "transform", "translate(" + (right - 400) + ")");
    rightgrip.setAttributeNS(ns.drag, "x", (right - 400));
    rightgrip.setAttributeNS(ns.drag,"constraint-left",(-400 + left + (10 * 400 / NumDataPoints)));
    
    // change the scrollbar dimensions:
    var w = (right - left);

    scrollBar.setAttributeNS(null, "width", w);
    scrollBar.setAttributeNS(null, "transform", "translate(" + left + ")");

    scrollBar.setAttributeNS(ns.drag, "x", left);
    scrollBar.setAttributeNS(ns.drag, "constraint-left",0);
    scrollBar.setAttributeNS(ns.drag, "constraint-right", 400-w);
    
    reflowTimeBasedGraph(left,right);
}

function reflowYAxis() {
    // run through the raw data, determine the max Y (find the y-limit)
    // it's too bad there is no way around this 
    var max;
    
    // if we're in distribution mode the y axis is always set to 100%
    BrowserCurrentMaxY = 100;
    // otherwise we need to calculate the max Y from the browser traffic data
    if(!bDistribution) {
        BrowserCurrentMaxY = 0;
        for(var day = browserStartPoint; day <= browserEndPoint; ++day) {
            // determine maximum Y
            max = 0; 
            for(var browser in BrowserData) {
                if(BrowserEnables[browser] == false) continue;
                max += BrowserData[browser][day];
            }
            var max = parseInt(max/100 + 1) * 100;
            if(max > BrowserCurrentMaxY) { BrowserCurrentMaxY = max; }
        }
    }
    
    // upper nudge value for points of interest    
    BrowserCurrentMaxY += 5;

    // next, re-create the x and y axes elements
    var axes = $('browser_timechartaxes');
    var yaxis = drawTimeBasedYAxis(BrowserCurrentMaxY);
    var xaxis = drawTimeBasedXAxis(browserStartPoint,browserEndPoint);

    // next, remove the x and y axes
    removeChildren(axes);
    // next, add in the new x- and y- axes elements
    axes.appendChild(yaxis);
    axes.appendChild(xaxis);

    yReflowTimer = 0;
}

function reflowPie() {
    // next, determine if the horizontal cursor needs to be shifted
    var bNeedToRedrawPieChart = false;
    if(gCursorX < browserStartPoint) { 
        gCursorX = browserStartPoint; 
        bNeedToRedrawPieChart = true;
    }
    else if(gCursorX > browserEndPoint) { 
        gCursorX = browserEndPoint; 
        bNeedToRedrawPieChart = true;
    }

    // update horizontal cursor
    var cur = $("browser_horizcurs");
    if(cur) {
        var x = 400*(gCursorX-browserStartPoint)/(browserEndPoint-browserStartPoint);
        cur.setAttributeNS(null, "transform", "translate(" + x + ",0)");
        cur.setAttributeNS(ns.drag, "x", x);
    }
    if(bNeedToRedrawPieChart) {
        drawPieChart();
    }
}

function reflowTimeBasedGraph(left,right)
{
    console.log('reflowTimeBasedGraph, left='+left+', right='+right);
    // first, determine the data points that the grippies map to
    browserStartPoint = Math.round( (left/400) * (NumDataPoints-1) );
    browserEndPoint = Math.round( (right/400) * (NumDataPoints-1) );

    // next, re-transform the paths
    var numPts = (browserEndPoint - browserStartPoint);
    var scaleX = 400.0 / numPts;
    var scaleY = 300.0 / BrowserCurrentMaxY;
    // translateX by 400.0 will always align the right-most point (most recent) with
    // the right edge of the graph - we need to shove it further right by 
    // (NumDataPoints - 1 - browserEndPoint) points
    var nudge = (NumDataPoints - 1 - browserEndPoint)*400/numPts;
    var translateX = 400.0 + nudge;
    var translateY = 300.0;
    var graphUnscaled = $('browser_timechartseries');
    graphUnscaled.setAttributeNS(null, "transform", "translate(" + translateX + "," + translateY + ")  scale(-1,-1)");
    var graph = $('browser_timechartseries_scaled');
    graph.setAttributeNS(null, "transform", "scale(" + scaleX + "," + scaleY + ") ");
        
    // TODO: now re-plot points of interest at the right places
    drawPointsOfInterest(browserStartPoint, browserEndPoint);
}

function drawPointsOfInterest(startPoint, endPoint)
{
    // toggle off all points of interest in a loop
    var ptIndex = null;
    for(ptIndex in ptElems) {
      var pt = ptElems[ptIndex];
      
      // determine which points are visible and turn them on/off
      var bInView = (pt.day <= endPoint && pt.day >= startPoint);
      pt.elem.setAttributeNS(null, "display", (bInView ? "inline" : "none"));
      
      if(bInView) {
          var cx = 400 - (endPoint - pt.day) * 400 / (endPoint - startPoint);
          var cy = 300 - pt.y * (300.0 / BrowserCurrentMaxY);
          pt.elem.setAttributeNS(null, "cx", cx);
          pt.elem.setAttributeNS(null, "cy", cy);
      }
    }
}

// this function simply halts the event in its tracks and cancels bubbling
function preventBubble(evt) {
  evt.stopPropagation();
}

function growLegend(bGrow) {
  var outer = document.getElementById("legbordout");
  var inner = document.getElementById("legbordin");
  outer.setAttribute("pointer-events", bGrow ? "stroke" : "none");
  inner.setAttribute("pointer-events", bGrow ? "none" : "fill");
}

function drawLegend()
{
  var x = 10, y = 20, h = 11, w = 30, dy = 0, c; // dy = 5
  var g = $( 'browser_legend_scaler' );
  if( !g ) return;
  for(var browser in BrowserData)
  {
    c = cssname( browser );
    var checkbox = make( 'rect', { "id":(browser+".cb"), "x":x, "y":y, "width":10, "height":10, "fill": "white", "stroke": "black", "cursor": "pointer"} );
    checkbox.addEventListener("click", toggleBrowser, false);
//    checkbox.addEventListener("mouseover", preventBubble, false);
//    checkbox.addEventListener("mouseout", preventBubble, false);
    BrowserEnables[browser] = true;

    var check = make( 'polyline', { "id":(browser+".check"), "points": ( (x+2) + "," + (y+4) + " " + (x+5) + "," + (y+7) + " " + (x+8) + "," + (y+1) ), 
                      "stroke": "black", "stroke-width": 2, "fill": "none", "pointer-events": "none"} );
    g.appendChild( checkbox );
    g.appendChild( check );
    g.appendChild( make( 'rect', { "x":x+15, "y":y, "width":w, "height":h, "class":c, "pointer-events":"none"} ) );
    browser = browser.replace( '/', ' ' );
    g.appendChild( make( 'text', { "x":x+15+w+x, "y":y+8, "font-size":'10', "fill":'#DEDEDE', "pointer-events":"none" }, browser ) );
    y += h + dy;
  }

//  g = $( 'os_legend' );
//  y = 20;
//  for(var os in OSData)
//  {
//    c = cssname( os );
//    os = os.replace( '/', ' ' );
//    g.appendChild( make( 'rect', { "x":x, "y":y, "width":w, "height":h, "class":c } ) );
//    g.appendChild( make( 'text', { "x":x+w+x, "y":y+8, "font-size":'10', "fill":'#DEDEDE' }, os ) );
//    y += h + dy;
//  }
}

function toggleDistribution() {
    bDistribution = !bDistribution;

    // save and reset some variables    
    var left = parseFloat($('browser_leftgrip').getAttributeNS(ns.drag,"x"));
    var right = 400 + parseFloat($('browser_rightgrip').getAttributeNS(ns.drag,"x"));
    browserStartPoint = 0;
    browserEndPoint = NumDataPoints-1;

    // re-create entire chart
    createTimeBasedChart();
    drawPieChart();
    
    // reflow axes
    browserStartPoint = Math.round( (left/400) * (NumDataPoints-1) );
    browserEndPoint = Math.round( (right/400) * (NumDataPoints-1) );    
    reflowYAxis();

    // change area of focus
    changeGlobalXAxisBounds(left,right);
    
    return false;
}

function toggleBrowser(evt) {
    var browser = evt.currentTarget.id.substring(0,evt.currentTarget.id.lastIndexOf("."));
    BrowserEnables[browser] = !BrowserEnables[browser];
    $(browser+".check").setAttribute("visibility", (BrowserEnables[browser] ? "visible" : "hidden"));

    // save and reset some variables    
    var left = parseFloat($('browser_leftgrip').getAttributeNS(ns.drag,"x"));
    var right = 400 + parseFloat($('browser_rightgrip').getAttributeNS(ns.drag,"x"));
    browserStartPoint = 0;
    browserEndPoint = NumDataPoints-1;

    // re-create entire chart
    createTimeBasedChart();
    drawPieChart();
    
    // reflow axes
    browserStartPoint = Math.round( (left/400) * (NumDataPoints-1) );
    browserEndPoint = Math.round( (right/400) * (NumDataPoints-1) );    
    reflowYAxis();

    // change area of focus
    changeGlobalXAxisBounds(left,right);
    
    return false;
}

function showLegend() 
{
    var legend = $("browser_legend");
    if(legend) {
        legend.setAttributeNS(null, "display", "inline");
    }
//    legend = $("os_legend");
//    if(legend) {
//        legend.setAttributeNS(null, "display", "inline");
//    }
}

function hideLegend() 
{
    var legend = $("browser_legend");
    if(legend) {
        legend.setAttributeNS(null, "display", "none");
    }
//    legend = $("os_legend");
//    if(legend) {
//        legend.setAttributeNS(null, "display", "none");
//    }
}

// This function takes the raw data, creates stacked paths that map to it, 
// creates y and x-axis tick marks and labels, then wipes out "wholegraph" 
// children and appends everything to it
function createTimeBasedChart()
{
    // set up global x-axis (including grippies)
    var xaxisGlobal = null;

    xaxisGlobal = drawTimeBasedXAxis(0,NumDataPoints-1);
    xaxisGlobal.appendChild(make('line', {x1:0, y1:300, x2:400, y2:300, stroke:"black", "stroke-width":"3px"}));
    xaxisGlobal.appendChild(make('line', {x1:400, y1:300-5, x2:400, y2:300+5, stroke:"black", "stroke-width":"3px"}));

    var scrollBar = $('browser_scrollbar'); 
    enableDrag(scrollBar);
    scrollBar.setAttributeNS(ns.drag, "constraint-left",0);
    scrollBar.setAttributeNS(ns.drag, "constraint-top",0);
    scrollBar.setAttributeNS(ns.drag, "constraint-right", 0);
    scrollBar.setAttributeNS(ns.drag, "constraint-bottom",0);
    xaxisGlobal.appendChild(scrollBar);

    var leftGrip = make('g', {id:'browser_leftgrip', fill:'url(#startButt)'});
    var leftGrippie = make('use', {"xlink:href":"#grippie", transform:'translate(0,300)'});
    leftGrip.appendChild(leftGrippie);
    enableDrag(leftGrip);
    // override the default move cursor
    leftGrip.setAttributeNS(null, "cursor", "w-resize");
    leftGrip.setAttributeNS(ns.drag, "constraint-left",0);
    leftGrip.setAttributeNS(ns.drag, "constraint-top",0);
    leftGrip.setAttributeNS(ns.drag, "constraint-right",400);
    leftGrip.setAttributeNS(ns.drag, "constraint-bottom",0);
    xaxisGlobal.appendChild(leftGrip);

    var rightGrip = make('g', {id:'browser_rightgrip', fill:'url(#stopButt)'});
    var rightGrippie = make('use', {"xlink:href":"#grippie", transform:'translate(400,300)'});
    rightGrip.appendChild(rightGrippie);
    enableDrag(rightGrip);
    rightGrip.setAttributeNS(null, "cursor", "e-resize");
    rightGrip.setAttributeNS(ns.drag, "constraint-left",-400);
    rightGrip.setAttributeNS(ns.drag, "constraint-top",0);
    rightGrip.setAttributeNS(ns.drag, "constraint-right",0);
    rightGrip.setAttributeNS(ns.drag, "constraint-bottom",0);
    xaxisGlobal.appendChild(rightGrip);
    xaxisGlobal.setAttributeNS(null, "transform", "translate(0,40)");

    if(xaxisGlobal) {
        var g = $('browser_timechartglobalaxis');
        removeChildren(g);
        g.appendChild(xaxisGlobal);
    }

    // Do Browser stat
    var browser_axes = $('browser_timechartaxes');
    var browser_graphUnscaled = $('browser_timechartseries');
    var browser_graph = $('browser_timechartseries_scaled');
    var browser_overlay = $('browser_timechartseries_overlay');
    // remove any existing points in the overlay
    while(browser_overlay.firstChild) { browser_overlay.removeChild(browser_overlay.firstChild); }
    if(!browser_axes || !browser_graphUnscaled || !browser_graph) { return; }

    var browser_pathStrings = new Array();    
    var browser_newpaths = new Array();
    var browser_xaxis = null;
    var browser_yaxis = null;
    var browserOrder = new Array();
    var BrowserTotals = [];
    
    var numBrowsers = 0;
    for(var browser in BrowserData) {
        if(BrowserEnables[browser] == false) { continue; }
        browser_pathStrings[browser] = "M0,0 L";
        browserOrder[browser] = numBrowsers;
        ++numBrowsers;
    }

    BrowserCurrentMaxY = 0;
    // clear out any existing points
    ptElems = [];
    for( var day = browserEndPoint; day >= browserStartPoint; --day) {
        var ptNum = (browserEndPoint - day);
        var thisDaySumY = 0;
        // if we're not in distribution mode, then set this to 100 to cancel out the x100 multiplication below
        BrowserTotals[day] = 100;
        // if we're in distribution mode, then calculate the totals across all filtered browsers
        if(bDistribution) {
            BrowserTotals[day] = 0;
            for(browser in BrowserData) {
                if(BrowserEnables[browser] == false) { continue; }
                BrowserTotals[day] += BrowserData[browser][day];
            }
        }
        for(browser in BrowserData) {
            if(BrowserEnables[browser] == false) { continue; }
            // avoid divide by zero
            if(BrowserTotals[day]) 
                thisDaySumY += (BrowserData[browser][day] / BrowserTotals[day]) * 100;
            browser_pathStrings[browser] += (ptNum) + "," + thisDaySumY + " ";
        }

        // put in points of interest if they exist for this date
        var html = PlacesOfInterest[ Dates[day] ];
        if( html ) {
            var pt = make( "circle", {id: ("pt"+day), fill:"#ffa", stroke:"black", "stroke-width":1, r:2, cx:0, cy:0 } );
            
            pt.appendChild( make( 'animate', {attributeName: "stroke-width", from:1, to:1.5, dur:"350ms", begin:"mouseover", fill:"freeze"} ) );
            pt.appendChild( make( 'animate', {attributeName: "stroke-width", from:1.5, to:1, dur:"250ms", begin:"mouseout", fill:"freeze"} ) );
            pt.appendChild( make( 'animate', {attributeName: "r", from:2, to:3, dur:"350ms", begin:"mouseover", fill:"freeze"} ) );
            pt.appendChild( make( 'animate', {attributeName: "r", from:3, to:2, dur:"250ms", begin:"mouseout", fill:"freeze"} ) );
            pt.appendChild( make( 'animateColor', {attributeName: "fill", from:"#ffd", to:"#ff8", dur:"350ms", begin:"mouseover", fill:"freeze"} ) );
            pt.appendChild( make( 'animateColor', {attributeName: "fill", from:"#ff8", to:"#ffd", dur:"250ms", begin:"mouseout", fill:"freeze"} ) );
            
            var jobj = PlacesOfInterest[ Dates[day] ];
            var link = make( "a", {});
            link.setAttributeNS(ns.xlink, "href", jobj.link);
            link.setAttributeNS(ns.xlink, "title", jobj.tooltip);
            link.setAttributeNS(null, "target", "_blank");
            pt = link.appendChild(pt);
            browser_overlay.appendChild(link);
            ptElems[ptElems.length] = { elem: pt, day: day, y: thisDaySumY };
        }
        
    } // for each day

    reflowYAxis();
    
    drawPointsOfInterest(browserStartPoint, browserEndPoint);    

    for(browser in BrowserData) {
        if(BrowserEnables[browser] == false) { continue; }    
        browser_pathStrings[browser] += (browserEndPoint - browserStartPoint) + ",0 Z";
        // Firefox needs a title attribute "title":browser, 
        browser_newpaths[browserOrder[browser]] = make('path', {"class":cssname(browser), "d":browser_pathStrings[browser]});
        // Opera needs a <title> element
        var tit = make('title', null, browser);
        browser_newpaths[browserOrder[browser]].appendChild(tit);
    }

    browser_yaxis = drawTimeBasedYAxis(BrowserCurrentMaxY);
    browser_xaxis = drawTimeBasedXAxis(browserStartPoint,browserEndPoint);
    
    // all done, wipe out allpaths, add our new paths to it in reverse order
    removeChildren(browser_graph);
    for(var loop = numBrowsers - 1; loop >= 0; --loop) 
    {
        browser_graph.appendChild(browser_newpaths[loop]);
    }
    
    var browser_numPts = (browserEndPoint - browserStartPoint);
    var browser_scaleX = 400.0 / browser_numPts;
    var browser_scaleY = 300.0 / BrowserCurrentMaxY;
    var browser_translateX = 400.0;
    var browser_translateY = 300.0;
    browser_graphUnscaled.setAttributeNS(null, "transform", "translate(" + browser_translateX + "," + browser_translateY + ") scale(-1,-1)");
    browser_graph.setAttributeNS(null, "transform", "scale(" + browser_scaleX + "," + browser_scaleY + ") ");

    // add the axes
    removeChildren(browser_axes);
    browser_axes.appendChild(browser_yaxis);
    browser_axes.appendChild(browser_xaxis);
}

// returns SVG elements that form the Y-axis
function drawTimeBasedYAxis(maxY)
{
    var ele = make('g', {id:"yaxis"});
    
    // determine the y-ticks
    var yinc = 0;
    if(maxY > 5000000) { yinc = 1000000; }
    else if(maxY > 1000000) { yinc = 200000; }
    else if(maxY > 200000) { yinc = 40000; }
    else if(maxY > 40000) { yinc = 8000; }
    else if(maxY > 8000) { yinc = 1500; }
    else if(maxY > 1500) { yinc = 300; } // this is the level of traffic as of Jan 13 05
    else if(maxY > 300) { yinc = 50; }
    else if(maxY > 50) { yinc = 10; }
    else { yinc = 1; }

    // text labels for y-axis
    var y = 0;
    while(y < maxY) {
        var ycoord = (300 - 300 * y / maxY);
        ele.appendChild( make('line', {x1:(0-5), y1:ycoord, x2:(0+5), y2:ycoord, stroke:"black", "stroke-width":"3px"} ) );
                                       
        // faint line
        ele.appendChild( make('line', {x1:0, y1:ycoord, x2:400, y2:ycoord, stroke:"grey", "stroke-width":"1px", opacity:0.5}) );
        ele.appendChild( make('text', {x:(0-10), y:(ycoord+5), "text-anchor":"end", "font-size":12}, y));
                              
        y += yinc;
    }
    
    return ele;
}

// returns SVG elements that form the X-axis
function drawTimeBasedXAxis(start,end)
{
    // X-Axis

    // xticks will store an array of arrays where the first element is the label 
    // and the second element is which data point to display it against
    var xticks = new Array();
    var numPts = end - start;// + 1;
    
    var ele = make('g', {id:"xaxis"});
    
    if(numPts > (365*3/2)/7) {
        xticks[0] = [ Dates[start].substr(0,4), 0 ];
        
        // loop through $dates
        for(var ind = start; ind <= end; ++ind) {
            // do every year
            var year = Dates[ind].substr(0,4);
            // extract the first 4 digits as the year
            if( year != xticks[xticks.length-1][0] ) {
                xticks[xticks.length] = [ Dates[ind].substr(0,4), ind-start ];
            }
        } // for each date
    }
    // NOTE: These labels don't quite fit right at the edge, should adjust the numPts threshold
    else if(numPts > 180/7) {
        // do every quarter
        //                        Jan       Feb       Mar       Apr       May       June
        var quarters = [null, "Winter", "Winter", "Winter", "Spring", "Spring", "Spring", 
        //                    July      Aug      Sept    Oct    Nov      Dec
                            "Summer", "Summer", "Summer", "Fall", "Fall", "Fall"];
        
        // loop through Dates
        var quartstr = null;
        for(var ind = start; ind <= end; ++ind) {
            var year = Dates[ind].substr(2,2);
            var month = Dates[ind].substr(5,2);
            
            if(month.substr(0,1) == '0') { month = month.substr(1,1); }

            if(quarters[month] != quartstr) {
                quartstr = quarters[month];
                xticks[xticks.length] = [ (quartstr + year), ind-start ];
            }
        } // for each date
    }
    // NOTE: We've got extra room here, can enlarge the month window and decrease the quarter window
    else { //if(numPts > 90/7) {
        // do every month
        var months = [null, "Jan", "Feb", "Mar", "Apr", "May", "June", 
                        "July", "Aug", "Sept", "Oct", "Nov", "Dec"];

	var monthstr = null;
        // loop through Dates
        for(var ind = start; ind <= end; ++ind) {
            var year = Dates[ind].substr(2,2);
            var month = Dates[ind].substr(5,2);
            if(month.substr(0,1) == '0') { month = month.substr(1,1); }

            if(months[month] != monthstr) {
                monthstr = months[month];
                xticks[xticks.length] = [ (monthstr + year), ind-start ];
            }
        } // for each date
    }
    /* These two windows are useless now that I've reduced the resolution to 7 days 
    else if(numPts > 30/7) {
        // do every 10 days
        // loop through Dates
        xticks[0] = [ Dates[start].substr(5,5), 0 ];
        
        for(var ind = start; ind <= end; ++ind) {
            if(( (ind-start) % 10) == 0) {
                var day = Dates[ind].substr(5,5);
                xticks[xticks.length] = [ day, ind-start ];
            }
        } // for each date
    }
    else {
        // do every 5 days
        // loop through Dates
        xticks[0] = [ Dates[start].substr(5,5), 0 ];
        
        for(var ind = start; ind <= end; ++ind) {
            if(( (ind-start) % 5) == 0) {
                var day = Dates[ind].substr(5,5);
                xticks[xticks.length] = [ day, ind-start ];
            }
        } // for each date
    }*/
    
    // if the first xtick is too close to the second xtick, then we should remove the first xtick
    if(xticks.length > 2) {
        var delta12 = xticks[2][1] - xticks[1][1];
        var delta01 = xticks[1][1] - xticks[0][1];
        if(delta12 > delta01*2) {
            xticks.splice(0,1);
        }
    }
    
    ele.appendChild( make('line', {x1:0, y1:(300-5), x2:0, y2:(300+5), stroke:"black", "stroke-width":"3px"}) );
    ele.appendChild( make('line', {x1:400, y1:(300-5), x2:400, y2:(300+5), stroke:"black", "stroke-width":"3px"}) );
    for(var ind = 0; ind < xticks.length; ++ind) {
        ele.appendChild( make('line', {"x1":(400 * xticks[ind][1] / numPts),
                                       "y1":(300 - 5),
                                       "x2":(400 * xticks[ind][1] / numPts),
                                       "y2":(300 + 5),
                                       "stroke":"black", 
                                       "stroke-width":"3px"} ) );

        ele.appendChild( make('text', {"x":(400 * xticks[ind][1] / numPts),
                                       "y":(300 + 20), 
                                       "text-anchor":"middle",
                                       "font-size":"12"}, xticks[ind][0] ) );
    }
    
    return ele;
}


function PieElementsContainer() {
    var r = 125, cx = 150, cy = 150;
    
    this.elem = $('browser_piegroup'); //make('g', {id:'Top'});
//    this.elem.setAttributeNS(null, 'transform', 'translate(450,0)');//display', 'none');
//    $('browser_stat').appendChild(this.elem);
    
    this.elem.appendChild( make('circle', {id:'OuterRing', cx:cx, cy:cy, r:cx }) );
    this.AllWedges = this.elem.appendChild( make( 'g', {id:'AllWedges', 
                                                        transform:'rotate(90,150,150)'}) );
    var LabelG = make( 'g', {id:'LabelG'} );
    var ele = 0;
    var name;
    this.BrowserWedgePaths = new Object();
    this.BrowserPctLabels = new Object();
    this.BrowserVerLabels = new Object();
    this.BrowserFamilyLabels = new Object();
    for(var browser in BrowserData) {
        // create wedge path elements
        // for whatever reason, Firefox seems to want a title attribute (not even valid SVG!)
        var attr = { id:("WEDGE" + browser), 'title': browser, 'class':"wedge " + cssname(browser) };
        if(!ele) {
            attr['cx'] = cx; attr['cy']=cy; attr['r'] = r;
            ele = make( 'circle', attr );            
        }
        else {
            ele = make( 'path', attr );
        }
        // Opera needs a <title> element
        var tit = make('title', null, browser);
        ele.appendChild(tit);
        this.BrowserWedgePaths[browser] = this.AllWedges.appendChild(ele);
        
        // create labels (percentages and version)
        this.BrowserPctLabels[browser] = LabelG.appendChild(make('text', { 'class':'label pct'}, '%'));

        var version = browser.split( '/' )[1];
        if( version == 'Other' ) version = 'X';
        
        this.BrowserVerLabels[browser] = LabelG.appendChild(make('text', { 'class':'label version'}, version));
        
        name = browser.replace( /\/.*/, '' );
        if(!this.BrowserFamilyLabels[name]) {
            this.BrowserFamilyLabels[name] = LabelG.appendChild( make('text', {'class':'label browser'}, name) );
            this.BrowserFamilyLabels[name].appendChild( make('title', {}, '%') );
        }

    } // for(var browser in BrowserData)
    
    this.AllWedges.appendChild( LabelG );
    this.elem.appendChild( make('circle', {'id':'InnerRing', 'cx':cx, 'cy':cy, 'r':r-1, 'pointer-events': 'none' }) );
    this.elem.appendChild( make('circle', {'id':'BrVerFill', 'cx':cx, 'cy':cy, 'r':r-15, 'pointer-events': 'none' }) );
    this.elem.appendChild( make('circle', {'id':'BrPctFill', 'cx':cx, 'cy':cy, 'r':r-30, 'pointer-events': 'none' }) );    
    
    this.Title = this.elem.appendChild( make('text', {id:'PieTitle',x:150,y:325}, 'Distribution for '));
    this.SubTitle = this.elem.appendChild( make('text', {id:'PieSubTitle',x:150,y:340}, 'Total Hits: '));
}

var BrowserPieElements = null;

// TO DO:  Fix this function for sparse data (see soverygood.com)
function drawPieChart()
{
    var root = getRoot();
    
    if(root.suspendRedraw) {
        root.suspendRedraw(200);
    }
    
    var TotalBrowserHits = 0;
    for(var browser in BrowserData) {
        if(BrowserEnables[browser] == false) { continue; }
        TotalBrowserHits += BrowserData[browser][gCursorX]||0;
    }

    var pct = 0, prevBrowserPct = 0;
    var r = 125, cx = 150, cy = 150;
    var startx = 0, starty = 0, endx = 0, endy = 0;
    var theta = 0, delta = 0;
    var ele = 0;
    var attr = null;
    var pf = 26, vf = 13; // percent and version radial offsets, in pixels

    var browsers = {}, name;
  
    for(var browser in BrowserPieElements.BrowserWedgePaths) {
        BrowserPieElements.BrowserWedgePaths[browser].setAttributeNS(null, "display", "none");
        BrowserPieElements.BrowserPctLabels[browser].setAttributeNS(null, "display", "none");
        BrowserPieElements.BrowserVerLabels[browser].setAttributeNS(null, "display", "none");
    }
    
    for(var browser in BrowserData) {
        if(BrowserEnables[browser] == false) { continue; }
        pct = prevBrowserPct + (BrowserData[browser][gCursorX]||0) / TotalBrowserHits;
        if(pct == prevBrowserPct) { continue; }

        name = browser.replace( /\/.*/, '' );
        browsers[name] = (browsers[name]||0) + pct - prevBrowserPct;
    
        // starting point:
        theta = (2 * Math.PI) * prevBrowserPct;
        delta = -theta;
        startx = cx - Math.cos(theta) * r; // negative sign is to mirror horizontally
        starty = cy - Math.sin(theta) * r; // negative sign is because y is in the down direction

        // end point:
        theta = (2 * Math.PI) * pct;
        delta += theta;
        endx = cx - Math.cos(theta) * r; // negative sign is to mirror horizontally
        endy = cy - Math.sin(theta) * r; // negative sign is because y is in the down direction

        attr = { 'class':"wedge " + cssname(browser) };
         
        BrowserPieElements.BrowserWedgePaths[browser].setAttributeNS(null, "display", "inline");
        BrowserPieElements.BrowserWedgePaths[browser].setAttributeNS(null, "d", 
                  "M" + startx + "," + starty + " " + // elliptical arc to the end point
                  "A" + r + "," + r + " 0 " +  // radii and x-axis-rotation
                  ( (delta > Math.PI || delta < 0) ? 1 : 0 ) + // large-arc-flag 
                  " 1 " + // sweep-flag
                  endx + "," + endy + " " + 
                  "L" + cx + "," + cy + " Z");

        // Now draw percentage label 
        var thisPct = Math.round(100.0*(pct-prevBrowserPct));
        if(thisPct > 2.0) {
            theta -= ( (delta/2) );
            var lx = Math.cos(theta);
            var ly = Math.sin(theta);
            var angle = (theta * 180.0 / Math.PI) - 90;
            var version = browser.split( '/' )[1];
            if( version == 'Other' ) version = 'X';

            removeChildren(BrowserPieElements.BrowserPctLabels[browser]);
            BrowserPieElements.BrowserPctLabels[browser].appendChild(document.createTextNode(thisPct + "%"));
            var str = 'translate('+(cx-lx*(r-pf))+','+(cy-ly*(r-pf))+') ' + 'rotate('+ angle +') ';
            BrowserPieElements.BrowserPctLabels[browser].setAttributeNS(null, "display", "inline");
            BrowserPieElements.BrowserPctLabels[browser].setAttributeNS(null, "transform", str);

            str = 'translate('+(cx-lx*(r-vf))+','+(cy-ly*(r-vf))+') ' + 'rotate('+ angle +') ';
            BrowserPieElements.BrowserVerLabels[browser].setAttributeNS(null, "transform", str);
            BrowserPieElements.BrowserVerLabels[browser].setAttributeNS(null, "display", "inline");
        }

        prevBrowserPct = pct;

    } // for each series

    // Now draw browser name labels
    prevBrowserPct = pct = 0;
    for(var family in BrowserPieElements.BrowserFamilyLabels) {
        BrowserPieElements.BrowserFamilyLabels[family].setAttributeNS(null, "display", "none");
    }
    for(var browser in browsers)
    {
        pct = browsers[browser];
        theta = (prevBrowserPct + pct/2) * Math.PI * 2;
        var labx = cx - Math.cos(theta) * (r+7.8);
        var laby = cy - Math.sin(theta) * (r+7.8);
        
        var str = 'translate('+ labx +','+ laby +') '+
                    'rotate('+ ((theta * 180.0 / Math.PI) - 90) +') ';
        var pctStr = Math.round(100.0*pct) + "%";
        var elem = BrowserPieElements.BrowserFamilyLabels[browser];
        elem.setAttributeNS(null, "transform", str);
        elem.setAttributeNS(null, "display", "inline");
        elem.setAttributeNS(null, "title", pctStr);
        elem.removeChild(elem.lastChild);
        elem.appendChild( make( 'title', {}, pctStr ) );
        
        prevBrowserPct += pct;
    }

    // add titles
    var date = Dates[gCursorX];
                         
    removeChildren(BrowserPieElements.Title);
    BrowserPieElements.Title.appendChild(document.createTextNode('Distribution for ' + date));
    
    removeChildren(BrowserPieElements.SubTitle);
    BrowserPieElements.SubTitle.appendChild(document.createTextNode('Total Browser Hits: ' + TotalBrowserHits));
    
    if(root.unsuspendRedrawAll) {
        root.unsuspendRedrawAll();
    }
} // end drawPieChart()
