/** * -------------------------------------------------------------------- * jQuery-Plugin "visualize" * by Scott Jehl, scott@filamentgroup.com * http://www.filamentgroup.com * Copyright (c) 2009 Filament Group * Dual licensed under the MIT (filamentgroup.com/examples/mit-license.txt) and GPL (filamentgroup.com/examples/gpl-license.txt) licenses. * * -------------------------------------------------------------------- */ (function($) { $.fn.visualize = function(options, container){ return $(this).each(function(){ //configuration var o = $.extend({ type: 'bar', //also available: area, pie, line width: $(this).width(), //height of canvas - defaults to table height height: $(this).height(), //height of canvas - defaults to table height appendTitle: true, //table caption text is added to chart title: null, //grabs from table caption if null appendKey: true, //color key is added to chart colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'], textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS parseDirection: 'x', //which direction to parse the table data pieMargin: 10, //pie charts only - spacing around pie pieLabelsAsPercent: true, pieLabelPos: 'inside', lineWeight: 4, //for line and area - stroke weight lineDots: false, //also available: 'single', 'double' dotInnerColor: "#ffffff", // only used for lineDots:'double' lineMargin: (options.lineDots?15:0), //for line and area - spacing around lines barGroupMargin: 10, chartId: '', xLabelParser: null, // function to parse labels as values valueParser: null, // function to parse values. must return a Number chartId: '', chartClass: '', barMargin: 1, //space around bars in bar chart (added to both sides of bar) yLabelInterval: 30, //distance between y labels interaction: false // only used for lineDots != false -- triggers mouseover and mouseout on original table },options); //reset width, height to numbers o.width = parseFloat(o.width); o.height = parseFloat(o.height); // reset padding if graph is not lines if(o.type != 'line' && o.type != 'area' ) { o.lineMargin = 0; } var self = $(this); // scrape data from html table var tableData = {}; var colors = o.colors; var textColors = o.textColors; var parseLabels = function(direction){ var labels = []; if(direction == 'x'){ self.find('thead tr').each(function(i){ $(this).find('th').each(function(j){ if(!labels[j]) { labels[j] = []; } labels[j][i] = $(this).text() }) }); } else { self.find('tbody tr').each(function(i){ $(this).find('th').each(function(j) { if(!labels[i]) { labels[i] = []; } labels[i][j] = $(this).text() }); }); } return labels; }; var fnParse = o.valueParser || parseFloat; var dataGroups = tableData.dataGroups = []; if(o.parseDirection == 'x'){ self.find('tbody tr').each(function(i,tr){ dataGroups[i] = {}; dataGroups[i].points = []; dataGroups[i].color = colors[i]; if(textColors[i]){ dataGroups[i].textColor = textColors[i]; } $(tr).find('td').each(function(j,td){ dataGroups[i].points.push( { value: fnParse($(td).text()), elem: td, tableCords: [i,j] } ); }); }); } else { var cols = self.find('tbody tr:eq(0) td').size(); for(var i=0; itableData.topValue) { tableData.topValue = fnParse(item.value,10); } if(item.valuexTopValue) { xTopValue = label; } if(label tableData.topValue+loopInterval) { yLabels.pop(); } else if (yLabels[yLabels.length-1] <= tableData.topValue-10) { yLabels.push(tableData.topValue); } // populate some data $.each(dataGroups,function(i,row){ row.yLabels = tableData.yAllLabels[i]; $.each(row.points, function(j,point){ point.zeroLocY = tableData.zeroLocY; point.zeroLocX = tableData.zeroLocX; point.xLabels = tableData.xAllLabels[j]; point.yLabels = tableData.yAllLabels[i]; point.color = row.color; }); }); try{console.log(tableData);}catch(e){} var charts = {}; charts.pie = { interactionPoints: dataGroups, setup: function() { charts.pie.draw(true); }, draw: function(drawHtml){ var centerx = Math.round(canvas.width()/2); var centery = Math.round(canvas.height()/2); var radius = centery - o.pieMargin; var counter = 0.0; if(drawHtml) { canvasContain.addClass('visualize-pie'); if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); } var toRad = function(integer){ return (Math.PI/180)*integer; }; var labels = $('
    ') .insertAfter(canvas); } //draw the pie pieces $.each(dataGroups, function(i,row){ var fraction = row.groupTotal / dataSum; if (fraction <= 0 || isNaN(fraction)) return; ctx.beginPath(); ctx.moveTo(centerx, centery); ctx.arc(centerx, centery, radius, counter * Math.PI * 2 - Math.PI * 0.5, (counter + fraction) * Math.PI * 2 - Math.PI * 0.5, false); ctx.lineTo(centerx, centery); ctx.closePath(); ctx.fillStyle = dataGroups[i].color; ctx.fill(); // draw labels if(drawHtml) { var sliceMiddle = (counter + fraction/2); var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5; var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance)); var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) * (distance)); var leftRight = (labelx > centerx) ? 'right' : 'left'; var topBottom = (labely > centery) ? 'bottom' : 'top'; var percentage = parseFloat((fraction*100).toFixed(2)); // interaction variables row.canvasCords = [labelx,labely]; row.zeroLocY = tableData.zeroLocY = 0; // related to zeroLocY and plugin API row.zeroLocX = tableData.zeroLocX = 0; // related to zeroLocX and plugin API row.value = row.groupTotal; if(percentage){ var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : row.groupTotal; var labeltext = $('' + labelval +'') .css(leftRight, 0) .css(topBottom, 0); if(labeltext) var label = $('
  • ') .appendTo(labels) .css({left: labelx, top: labely}) .append(labeltext); labeltext .css('font-size', radius / 8) .css('margin-'+leftRight, -labeltext.width()/2) .css('margin-'+topBottom, -labeltext.outerHeight()/2); if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); } } } counter+=fraction; }); } }; (function(){ var xInterval; var drawPoint = function (ctx,x,y,color,size) { ctx.moveTo(x,y); ctx.beginPath(); ctx.arc(x,y,size/2,0,2*Math.PI,false); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); }; charts.line = { interactionPoints: allItems, setup: function(area){ if(area){ canvasContain.addClass('visualize-area'); } else{ canvasContain.addClass('visualize-line'); } //write X labels var xlabelsUL = $('
      ') .width(canvas.width()) .height(canvas.height()) .insertBefore(canvas); if(!o.customXLabels) { xInterval = (canvas.width() - 2*o.lineMargin) / (xLabels.length -1); $.each(xLabels, function(i){ var thisLi = $('
    • '+this+'
    • ') .prepend('') .css('left', o.lineMargin + xInterval * i) .appendTo(xlabelsUL); var label = thisLi.find('span:not(.line)'); var leftOffset = label.width()/-2; if(i == 0){ leftOffset = 0; } else if(i== xLabels.length-1){ leftOffset = -label.width(); } label .css('margin-left', leftOffset) .addClass('label'); }); } else { o.customXLabels(tableData,xlabelsUL); } //write Y labels var liBottom = (canvas.height() - 2*o.lineMargin) / (yLabels.length-1); var ylabelsUL = $('
        ') .width(canvas.width()) .height(canvas.height()) // .css('margin-top',-o.lineMargin) .insertBefore(scroller); $.each(yLabels, function(i){ var value = Math.floor(this); var posB = (value-bottomValue)*yScale + o.lineMargin; if(posB >= o.height-1 || posB < 0) { return; } var thisLi = $('
      • '+value+'
      • ') .css('bottom', posB); if(Math.abs(posB) < o.height-1) { thisLi.prepend(''); } thisLi.prependTo(ylabelsUL); var label = thisLi.find('span:not(.line)'); var topOffset = label.height()/-2; if(!o.lineMargin) { if(i == 0){ topOffset = -label.height(); } else if(i== yLabels.length-1){ topOffset = 0; } } label .css('margin-top', topOffset) .addClass('label'); }); //start from the bottom left ctx.translate(zeroLocX,zeroLocY); charts.line.draw(area); }, draw: function(area) { // prevent drawing on top of previous draw ctx.clearRect(-zeroLocX,-zeroLocY,o.width,o.height); // Calculate each point properties before hand var integer; $.each(dataGroups,function(i,row){ integer = o.lineMargin; // the current offset $.each(row.points, function(j,point){ if(o.xLabelParser) { point.canvasCords = [(xLabels[j]-zeroLocX)*xScale - xBottomValue,-(point.value*yScale)]; } else { point.canvasCords = [integer,-(point.value*yScale)]; } if(o.lineDots) { point.dotSize = o.dotSize||o.lineWeight*Math.PI; point.dotInnerSize = o.dotInnerSize||o.lineWeight*Math.PI/2; if(o.lineDots == 'double') { point.innerColor = o.dotInnerColor; } } integer+=xInterval; }); }); // fire custom event so we can enable rich interaction self.trigger('vizualizeBeforeDraw',{options:o,table:self,canvasContain:canvasContain,tableData:tableData}); // draw lines and areas $.each(dataGroups,function(h){ // Draw lines ctx.beginPath(); ctx.lineWidth = o.lineWeight; ctx.lineJoin = 'round'; $.each(this.points, function(g){ var loc = this.canvasCords; if(g == 0) { ctx.moveTo(loc[0],loc[1]); } ctx.lineTo(loc[0],loc[1]); }); ctx.strokeStyle = this.color; ctx.stroke(); // Draw fills if(area){ var integer = this.points[this.points.length-1].canvasCords[0]; if (isFinite(integer)) ctx.lineTo(integer,0); ctx.lineTo(o.lineMargin,0); ctx.closePath(); ctx.fillStyle = this.color; ctx.globalAlpha = .3; ctx.fill(); ctx.globalAlpha = 1.0; } else {ctx.closePath();} }); // draw points if(o.lineDots) { $.each(dataGroups,function(h){ $.each(this.points, function(g){ drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.color,this.dotSize); if(o.lineDots === 'double') { drawPoint(ctx,this.canvasCords[0],this.canvasCords[1],this.innerColor,this.dotInnerSize); } }); }); } } }; })(); charts.area = { setup: function() { charts.line.setup(true); }, draw: charts.line.draw }; (function(){ var horizontal,bottomLabels; charts.bar = { setup:function(){ /** * We can draw horizontal or vertical bars depending on the * value of the 'barDirection' option (which may be 'vertical' or * 'horizontal'). */ horizontal = (o.barDirection == 'horizontal'); canvasContain.addClass('visualize-bar'); /** * Write labels along the bottom of the chart. If we're drawing * horizontal bars, these will be the yLabels, otherwise they * will be the xLabels. The positioning also varies slightly: * yLabels are values, hence they will span the whole width of * the canvas, whereas xLabels are supposed to line up with the * bars. */ bottomLabels = horizontal ? yLabels : xLabels; var xInterval = canvas.width() / (bottomLabels.length - (horizontal ? 1 : 0)); var xlabelsUL = $('
          ') .width(canvas.width()) .height(canvas.height()) .insertBefore(canvas); $.each(bottomLabels, function(i){ var thisLi = $('
        • '+this+'
        • ') .prepend('') .css('left', xInterval * i) .width(xInterval) .appendTo(xlabelsUL); if (horizontal) { var label = thisLi.find('span.label'); label.css("margin-left", -label.width() / 2); } }); /** * Write labels along the left of the chart. Follows the same idea * as the bottom labels. */ var leftLabels = horizontal ? xLabels : yLabels; var liBottom = canvas.height() / (leftLabels.length - (horizontal ? 0 : 1)); var ylabelsUL = $('
            ') .width(canvas.width()) .height(canvas.height()) .insertBefore(canvas); $.each(leftLabels, function(i){ var thisLi = $('
          • '+this+'
          • ').prependTo(ylabelsUL); var label = thisLi.find('span:not(.line)').addClass('label'); if (horizontal) { /** * For left labels, we want to vertically align the text * to the middle of its container, but we don't know how * many lines of text we will have, since the labels could * be very long. * * So we set a min-height of liBottom, and a max-height * of liBottom + 1, so we can then check the label's actual * height to determine if it spans one line or more lines. */ label.css({ 'min-height': liBottom, 'max-height': liBottom + 1, 'vertical-align': 'middle' }); thisLi.css({'top': liBottom * i, 'min-height': liBottom}); var r = label[0].getClientRects()[0]; if (r.bottom - r.top == liBottom) { /* This means we have only one line of text; hence * we can centre the text vertically by setting the line-height, * as described at: * http://www.ampsoft.net/webdesign-l/vertical-aligned-nav-list.html * * (Although firefox has .height on the rectangle, IE doesn't, * so we use r.bottom - r.top rather than r.height.) */ label.css('line-height', parseInt(liBottom) + 'px'); } else { /* * If there is more than one line of text, then we shouldn't * touch the line height, but we should make sure the text * doesn't overflow the container. */ label.css("overflow", "hidden"); } } else { thisLi.css('bottom', liBottom * i).prepend(''); label.css('margin-top', -label.height() / 2) } }); charts.bar.draw(); }, draw: function() { // Draw bars if (horizontal) { // for horizontal, keep the same code, but rotate everything 90 degrees // clockwise. ctx.rotate(Math.PI / 2); } else { // for vertical, translate to the top left corner. ctx.translate(0, zeroLocY); } // Don't attempt to draw anything if all the values are zero, // otherwise we will get weird exceptions from the canvas methods. if (totalYRange <= 0) return; var yScale = (horizontal ? canvas.width() : canvas.height()) / totalYRange; var barWidth = horizontal ? (canvas.height() / xLabels.length) : (canvas.width() / (bottomLabels.length)); var linewidth = (barWidth - o.barGroupMargin*2) / dataGroups.length; for(var h=0; h')) .height(o.height) .width(o.width); var scroller = $('
            ') .appendTo(canvasContain) .append(canvas); //title/key container if(o.appendTitle || o.appendKey){ var infoContain = $('
            ') .appendTo(canvasContain); } //append title if(o.appendTitle){ $('
            '+ title +'
            ').appendTo(infoContain); } //append key if(o.appendKey){ var newKey = $('
              '); $.each(yAllLabels, function(i,label){ $('
            • '+ label +'
            • ') .appendTo(newKey); }); newKey.appendTo(infoContain); }; // init interaction if(o.interaction) { // sets the canvas to track interaction // IE needs one div on top of the canvas since the VML shapes prevent mousemove from triggering correctly. // Pie charts needs tracker because labels goes on top of the canvas and also messes up with mousemove var tracker = $('
              ') .css({ 'height': o.height + 'px', 'width': o.width + 'px', 'position':'relative', 'z-index': 200 }) .insertAfter(canvas); var triggerInteraction = function(overOut,data) { var data = $.extend({ canvasContain:canvasContain, tableData:tableData },data); self.trigger('vizualize'+overOut,data); }; var over=false, last=false, started=false; tracker.mousemove(function(e){ var x,y,x1,y1,data,dist,i,current,selector,zLabel,elem,color,minDist,found,ev=e.originalEvent; // get mouse position relative to the tracker/canvas x = ev.layerX || ev.offsetX || 0; y = ev.layerY || ev.offsetY || 0; found = false; minDist = started?30000:(o.type=='pie'?(Math.round(canvas.height()/2)-o.pieMargin)/3:o.lineWeight*4); // iterate datagroups to find points with matching $.each(charts[o.type].interactionPoints,function(i,current){ x1 = current.canvasCords[0] + zeroLocX; y1 = current.canvasCords[1] + (o.type=="pie"?0:zeroLocY); dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) ); if(dist < minDist) { found = current; minDist = dist; } }); if(o.multiHover && found) { x = found.canvasCords[0] + zeroLocX; y = found.canvasCords[1] + (o.type=="pie"?0:zeroLocY); found = [found]; $.each(charts[o.type].interactionPoints,function(i,current){ if(current == found[0]) {return;} x1 = current.canvasCords[0] + zeroLocX; y1 = current.canvasCords[1] + zeroLocY; dist = Math.sqrt( (x1 - x)*(x1 - x) + (y1 - y)*(y1 - y) ); if(dist <= o.multiHover) { found.push(current); } }); } // trigger over and out only when state changes, instead of on every mousemove over = found; if(over != last) { if(over) { if(last) { triggerInteraction('Out',{point:last}); } triggerInteraction('Over',{point:over}); last = over; } if(last && !over) { triggerInteraction('Out',{point:last}); last=false; } started=true; } }); tracker.mouseleave(function(){ triggerInteraction('Out',{ point:last, mouseOutGraph:true }); over = (last = false); }); } //append new canvas to page if(!container){canvasContain.insertAfter(this); } if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); } //set up the drawing board var ctx = canvas[0].getContext('2d'); // Scroll graphs scroller.scrollLeft(o.width-scroller.width()); // init plugins $.each($.visualizePlugins,function(i,plugin){ plugin.call(self,o,tableData); }); //create chart charts[o.type].setup(); if(!container){ //add event for updating self.bind('visualizeRefresh', function(){ self.visualize(o, $(this).empty()); }); //add event for redraw self.bind('visualizeRedraw', function(){ charts[o.type].draw(); }); } }).next(); //returns canvas(es) }; // create array for plugins. if you wish to make a plugin, // just push your init funcion into this array $.visualizePlugins = []; })(jQuery);