taxr/taxr.js

// taxr.js
// Sample JavaScript solution for Homework 5: Taxr
// INFO 343, Autumn 2012
// Morgan Doocy

(function() {
   var WEB_SERVICE = 'taxr.php';
   
   $.ajaxSetup({ error: ajaxError });
   
   // When the DOM has finished loading, fetch and build the slider based on
   // year data received from the web service.
   $(document).ready(function() {
      $.get(WEB_SERVICE, { format: 'json' }, function(years) {
         var minyear = years[0].year;
         var maxyear = years[years.length - 1].year;
         
         // Initialize slider element and label the handle.
         $('#curyear').text(minyear);
         $('#slider').slider({
            min: minyear,
            max: maxyear,
            step: 1,
            slide: function(event, ui) {
               $('#curyear').text(ui.value);
               fetchData(ui.value);
            }
         });
         $('#slider a').append($('#curyear').detach());
         
         // Generate blue/red blackground "sparkline" chart, indicating the maximum
         // tax rate and party of the president for each year (slider stop point).
         var slider_width = parseInt($('#slider').css('width'));
         var column_width = 1 / (years.length-1) * 100;
         for (var i = 0; i < years.length; i++) {
            var year = years[i].year;
            var $col = $('<li>').css({
               width: column_width + '%',
               left: (column_width * i) + '%',
               height: years[i].maxrate / 5,
               'margin-left': (-(column_width) / 2) + '%'
            }).addClass(years[i].party);
            if (i == 0 || i == years.length - 1 || (year < 2010 && year % 10 == 0)) {
               $('<label>').text(year).appendTo($col);
               $('<hr>').addClass('rule').appendTo($col);
            }
            $col.appendTo('#slider');
         }
         
         // Hide loading gif and move into slider handle.
         $('#loading').detach().appendTo('#slider a').hide();
      });
      
      // Re-fetch data when 'adjusted' checkbox selected/cleared.
      $('#adjusted').change(function() {
         fetchData($('#slider').slider('value'));
      });
      
      // Ensure details tooltip doesn't capture mouseover instead of rectangle behind it.
      $('#details').mouseover(function() {
         $(this).hide();
      });
   });
   
   // Fetch tax data for the given year in JSON format.
   function fetchData(year) {
      $('#loading').show();
      $.get(WEB_SERVICE, {
         dollars: $('#adjusted').is(':checked') ? 'adjusted' : 'nominal',
         format: 'json',
         year: year
      }, injectData);
   }
   
   // Inject the returned tax data into the page.
   function injectData(data) {
      $('#filing_statuses').empty();
      
      // Establish maximum rate of all brackets this year.
      var max = 0;
      $.each(data.statuses, function(status, brackets) {
         var highest = parseInt(brackets[brackets.length - 1].from);
         max = highest > max ? highest : max;
      });
      
      // Create and inject a label for this status, and boxes for each bracket
      // in this status.
      $.each(data.statuses, function(status, brackets) {
         $('<dt>').text(status).appendTo('#filing_statuses');
         var $dd = $('<dd>');
         var $ul = $('<ul>');
         $.each(brackets, function(i, bracket) {
            var height = bracket.rate;
            var left = parseInt((bracket.from) / 2000);
            var width;
            if (i == brackets.length - 1) {
               width = parseInt((max - bracket.from) / 2000);
               width = (left + width) < 1000 ? 1000 - left : width;
            } else {
               width = parseInt((bracket.to - bracket.from) / 2000);
            }
            // console.log("Bracket for " + bracket.rate + "% is " + height + "px tall, " + width + "px (= " + bracket.to + " - " + bracket.from + " / " + max + ") wide, and " + left + "px from the left");
            var $rate = $('<label>').addClass('rate').text(bracket.rate);
            var $from = $('<label>').addClass('from').text(parseInt(bracket.from / 1000));
            $('<li>').mousemove(bracketMove).mouseover(bracketOver).mouseout(bracketOut).css({
               'height': height,
               'width': width,
               'left': left
            }).append($rate).append($from).appendTo($ul);
         });
         $ul.appendTo($dd.appendTo('#filing_statuses'));
      });
      $('#loading').hide();
   }
   
   // Update the details tooltip with correct information based on the cursor
   // location inside the hovered bracket.
   function bracketMove(event) {
      var $target = $(event.target);
      if (event.target.nodeName == 'LI' && !$target.hasClass('current')) {
         var offsetParent = $target.parent().offset();
         
         var x = event.pageX;
         var y = event.pageY;
         
         var income = parseInt(x - offsetParent.left) * 2000;
         var rate = $target.find('label.rate').text();
         // console.log("income: " + income + " rate: " + rate + "%");
         var taxburden = 0;
         $target.prevAll().each(function(i, elem) {
            var $elem = $(elem);
            var rate = $elem.find('label.rate').text();
            var subincome = $elem.next().find('label.from').text() - $elem.find('label.from').text();
            // console.log("subincome: " + (subincome * 1000) + " rate: " + rate + "%");
            taxburden += (rate / 100) * (subincome * 1000);
         });
         taxburden += (rate / 100) * (income - ($target.find('label.from').text() * 1000));
         // console.log("remainder: " + (income - ($target.find('label.from').text() * 1000)));
         taxburden = Math.round(taxburden);
         
         $('#income').text('$' + commaSeparateNumber(income));
         $('#marginaltax').text(rate + '%');
         $('#taxburden').text('$' + commaSeparateNumber(taxburden));
         $('#averagetax').text(Math.round(taxburden / income * 100) + '%');
         
         $('#details').css({
            'left': x + 10 + 'px',
            'top': y + 10 + 'px'
         }).show();
      }
   }
   
   // Return the given integer formatted as comma-separated groups of three digits.
   function commaSeparateNumber(val){
      while (/(\d+)(\d{3})/.test(val.toString())){
         val = val.toString().replace(/(\d+)(\d{3})/, '$1'+','+'$2');
      }
      return val;
   }
   
   // Highlight this and all previous brackets when moused over.
   function bracketOver(event) {
      if (event.target.nodeName == 'LI') {
         $(this).prevAll().andSelf().addClass('over');
      }
   }
   
   // Unhighlight all brackets in this filing status when moused out.
   function bracketOut(event) {
      if (event.target.nodeName == 'LI') {
         $(this).prevAll().andSelf().removeClass('over');
         $('#details').hide();
      }
   }
   
   // Display a useful error message on Ajax failure.
   function ajaxError(jqxhr, type, error) {
      var msg = "An Ajax error occurred!\n\n";
      if (type == 'error') {
         if (jqxhr.readyState == 0) {
            // Request was never made - security block?
            msg += "Looks like the browser security-blocked the request.";
         } else {
            // Probably an HTTP error.
            msg += 'Error code: ' + jqxhr.status + "\n" + 
                      'Error text: ' + error + "\n" + 
                      'Full content of response: \n\n' + jqxhr.responseText;
         }
      } else {
         msg += 'Error type: ' + type;
         if (error != "") {
            msg += "\nError text: " + error;
         }
      }
      alert(msg);
   }
})();