visualize.jQuery.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463
  1. /*
  2. * --------------------------------------------------------------------
  3. * jQuery inputToButton plugin
  4. * Author: Scott Jehl, scott@filamentgroup.com
  5. * Copyright (c) 2009 Filament Group
  6. * licensed under MIT (filamentgroup.com/examples/mit-license.txt)
  7. * --------------------------------------------------------------------
  8. */
  9. (function($) {
  10. $.fn.visualize = function(options, container){
  11. return $(this).each(function(){
  12. //configuration
  13. var o = $.extend({
  14. type: 'bar', //also available: area, pie, line
  15. width: $(this).width(), //height of canvas - defaults to table height
  16. height: $(this).height(), //height of canvas - defaults to table height
  17. appendTitle: true, //table caption text is added to chart
  18. title: null, //grabs from table caption if null
  19. appendKey: true, //color key is added to chart
  20. rowFilter: ' ',
  21. colFilter: ' ',
  22. colors: ['#be1e2d','#666699','#92d5ea','#ee8310','#8d10ee','#5a3b16','#26a4ed','#f45a90','#e9e744'],
  23. textColors: [], //corresponds with colors array. null/undefined items will fall back to CSS
  24. parseDirection: 'x', //which direction to parse the table data
  25. pieMargin: 20, //pie charts only - spacing around pie
  26. pieLabelsAsPercent: true,
  27. pieLabelPos: 'inside',
  28. lineWeight: 4, //for line and area - stroke weight
  29. barGroupMargin: 10,
  30. barMargin: 1, //space around bars in bar chart (added to both sides of bar)
  31. yLabelInterval: 30 //distance between y labels
  32. },options);
  33. //reset width, height to numbers
  34. o.width = parseFloat(o.width);
  35. o.height = parseFloat(o.height);
  36. var self = $(this);
  37. //function to scrape data from html table
  38. function scrapeTable(){
  39. var colors = o.colors;
  40. var textColors = o.textColors;
  41. var tableData = {
  42. dataGroups: function(){
  43. var dataGroups = [];
  44. if(o.parseDirection == 'x'){
  45. self.find('tr:gt(0)').filter(o.rowFilter).each(function(i){
  46. dataGroups[i] = {};
  47. dataGroups[i].points = [];
  48. dataGroups[i].color = colors[i];
  49. if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
  50. $(this).find('td').filter(o.colFilter).each(function(){
  51. dataGroups[i].points.push( parseFloat($(this).text()) );
  52. });
  53. });
  54. }
  55. else {
  56. var cols = self.find('tr:eq(1) td').filter(o.colFilter).size();
  57. for(var i=0; i<cols; i++){
  58. dataGroups[i] = {};
  59. dataGroups[i].points = [];
  60. dataGroups[i].color = colors[i];
  61. if(textColors[i]){ dataGroups[i].textColor = textColors[i]; }
  62. self.find('tr:gt(0)').filter(o.rowFilter).each(function(){
  63. dataGroups[i].points.push( $(this).find('td').filter(o.colFilter).eq(i).text()*1 );
  64. });
  65. };
  66. }
  67. return dataGroups;
  68. },
  69. allData: function(){
  70. var allData = [];
  71. $(this.dataGroups()).each(function(){
  72. allData.push(this.points);
  73. });
  74. return allData;
  75. },
  76. dataSum: function(){
  77. var dataSum = 0;
  78. var allData = this.allData().join(',').split(',');
  79. $(allData).each(function(){
  80. dataSum += parseFloat(this);
  81. });
  82. return dataSum
  83. },
  84. topValue: function(){
  85. var topValue = 0;
  86. var allData = this.allData().join(',').split(',');
  87. $(allData).each(function(){
  88. if(parseFloat(this,10)>topValue) topValue = parseFloat(this);
  89. });
  90. return topValue;
  91. },
  92. bottomValue: function(){
  93. var bottomValue = 0;
  94. var allData = this.allData().join(',').split(',');
  95. $(allData).each(function(){
  96. if(this<bottomValue) bottomValue = parseFloat(this);
  97. });
  98. return bottomValue;
  99. },
  100. memberTotals: function(){
  101. var memberTotals = [];
  102. var dataGroups = this.dataGroups();
  103. $(dataGroups).each(function(l){
  104. var count = 0;
  105. $(dataGroups[l].points).each(function(m){
  106. count +=dataGroups[l].points[m];
  107. });
  108. memberTotals.push(count);
  109. });
  110. return memberTotals;
  111. },
  112. yTotals: function(){
  113. var yTotals = [];
  114. var dataGroups = this.dataGroups();
  115. var loopLength = this.xLabels().length;
  116. for(var i = 0; i<loopLength; i++){
  117. yTotals[i] =[];
  118. var thisTotal = 0;
  119. $(dataGroups).each(function(l){
  120. yTotals[i].push(this.points[i]);
  121. });
  122. yTotals[i].join(',').split(',');
  123. $(yTotals[i]).each(function(){
  124. thisTotal += parseFloat(this);
  125. });
  126. yTotals[i] = thisTotal;
  127. }
  128. return yTotals;
  129. },
  130. topYtotal: function(){
  131. var topYtotal = 0;
  132. var yTotals = this.yTotals().join(',').split(',');
  133. $(yTotals).each(function(){
  134. if(parseFloat(this,10)>topYtotal) topYtotal = parseFloat(this);
  135. });
  136. return topYtotal;
  137. },
  138. totalYRange: function(){
  139. return this.topValue() - this.bottomValue();
  140. },
  141. xLabels: function(){
  142. var xLabels = [];
  143. if(o.parseDirection == 'x'){
  144. self.find('tr:eq(0) th').filter(o.colFilter).each(function(){
  145. xLabels.push($(this).html());
  146. });
  147. }
  148. else {
  149. self.find('tr:gt(0) th').filter(o.rowFilter).each(function(){
  150. xLabels.push($(this).html());
  151. });
  152. }
  153. return xLabels;
  154. },
  155. yLabels: function(){
  156. var yLabels = [];
  157. yLabels.push(bottomValue);
  158. var numLabels = Math.round(o.height / o.yLabelInterval);
  159. var loopInterval = Math.ceil(totalYRange / numLabels) || 1;
  160. while( yLabels[yLabels.length-1] < topValue - loopInterval){
  161. yLabels.push(yLabels[yLabels.length-1] + loopInterval);
  162. }
  163. yLabels.push(topValue);
  164. return yLabels;
  165. }
  166. };
  167. return tableData;
  168. };
  169. //function to create a chart
  170. var createChart = {
  171. pie: function(){
  172. canvasContain.addClass('visualize-pie');
  173. if(o.pieLabelPos == 'outside'){ canvasContain.addClass('visualize-pie-outside'); }
  174. var centerx = Math.round(canvas.width()/2);
  175. var centery = Math.round(canvas.height()/2);
  176. var radius = centery - o.pieMargin;
  177. var counter = 0.0;
  178. var toRad = function(integer){ return (Math.PI/180)*integer; };
  179. var labels = $('<ul class="visualize-labels"></ul>')
  180. .insertAfter(canvas);
  181. //draw the pie pieces
  182. $.each(memberTotals, function(i){
  183. var fraction = (this <= 0 || isNaN(this))? 0 : this / dataSum;
  184. ctx.beginPath();
  185. ctx.moveTo(centerx, centery);
  186. ctx.arc(centerx, centery, radius,
  187. counter * Math.PI * 2 - Math.PI * 0.5,
  188. (counter + fraction) * Math.PI * 2 - Math.PI * 0.5,
  189. false);
  190. ctx.lineTo(centerx, centery);
  191. ctx.closePath();
  192. ctx.fillStyle = dataGroups[i].color;
  193. ctx.fill();
  194. // draw labels
  195. var sliceMiddle = (counter + fraction/2);
  196. var distance = o.pieLabelPos == 'inside' ? radius/1.5 : radius + radius / 5;
  197. var labelx = Math.round(centerx + Math.sin(sliceMiddle * Math.PI * 2) * (distance));
  198. var labely = Math.round(centery - Math.cos(sliceMiddle * Math.PI * 2) * (distance));
  199. var leftRight = (labelx > centerx) ? 'right' : 'left';
  200. var topBottom = (labely > centery) ? 'bottom' : 'top';
  201. var percentage = parseFloat((fraction*100).toFixed(2));
  202. if(percentage){
  203. var labelval = (o.pieLabelsAsPercent) ? percentage + '%' : this;
  204. var labeltext = $('<span class="visualize-label">' + labelval +'</span>')
  205. .css(leftRight, 0)
  206. .css(topBottom, 0);
  207. if(labeltext)
  208. var label = $('<li class="visualize-label-pos"></li>')
  209. .appendTo(labels)
  210. .css({left: labelx, top: labely})
  211. .append(labeltext);
  212. labeltext
  213. .css('font-size', radius / 8)
  214. .css('margin-'+leftRight, -labeltext.width()/2)
  215. .css('margin-'+topBottom, -labeltext.outerHeight()/2);
  216. if(dataGroups[i].textColor){ labeltext.css('color', dataGroups[i].textColor); }
  217. }
  218. counter+=fraction;
  219. });
  220. },
  221. line: function(area){
  222. if(area){ canvasContain.addClass('visualize-area'); }
  223. else{ canvasContain.addClass('visualize-line'); }
  224. //write X labels
  225. var xInterval = canvas.width() / (xLabels.length -1);
  226. var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
  227. .width(canvas.width())
  228. .height(canvas.height())
  229. .insertBefore(canvas);
  230. $.each(xLabels, function(i){
  231. var thisLi = $('<li><span>'+this+'</span></li>')
  232. .prepend('<span class="line" />')
  233. .css('left', xInterval * i)
  234. .appendTo(xlabelsUL);
  235. var label = thisLi.find('span:not(.line)');
  236. var leftOffset = label.width()/-2;
  237. if(i == 0){ leftOffset = 0; }
  238. else if(i== xLabels.length-1){ leftOffset = -label.width(); }
  239. label
  240. .css('margin-left', leftOffset)
  241. .addClass('label');
  242. });
  243. //write Y labels
  244. var yScale = canvas.height() / totalYRange;
  245. var liBottom = canvas.height() / (yLabels.length-1);
  246. var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
  247. .width(canvas.width())
  248. .height(canvas.height())
  249. .insertBefore(canvas);
  250. $.each(yLabels, function(i){
  251. var thisLi = $('<li><span>'+this+'</span></li>')
  252. .prepend('<span class="line" />')
  253. .css('bottom',liBottom*i)
  254. .prependTo(ylabelsUL);
  255. var label = thisLi.find('span:not(.line)');
  256. var topOffset = label.height()/-2;
  257. if(i == 0){ topOffset = -label.height(); }
  258. else if(i== yLabels.length-1){ topOffset = 0; }
  259. label
  260. .css('margin-top', topOffset)
  261. .addClass('label');
  262. });
  263. //start from the bottom left
  264. ctx.translate(0,zeroLoc);
  265. //iterate and draw
  266. $.each(dataGroups,function(h){
  267. ctx.beginPath();
  268. ctx.lineWidth = o.lineWeight;
  269. ctx.lineJoin = 'round';
  270. var points = this.points;
  271. var integer = 0;
  272. ctx.moveTo(0,-(points[0]*yScale));
  273. $.each(points, function(){
  274. ctx.lineTo(integer,-(this*yScale));
  275. integer+=xInterval;
  276. });
  277. ctx.strokeStyle = this.color;
  278. ctx.stroke();
  279. if(area){
  280. ctx.lineTo(integer,0);
  281. ctx.lineTo(0,0);
  282. ctx.closePath();
  283. ctx.fillStyle = this.color;
  284. ctx.globalAlpha = .3;
  285. ctx.fill();
  286. ctx.globalAlpha = 1.0;
  287. }
  288. else {ctx.closePath();}
  289. });
  290. },
  291. area: function(){
  292. createChart.line(true);
  293. },
  294. bar: function(){
  295. canvasContain.addClass('visualize-bar');
  296. //write X labels
  297. var xInterval = canvas.width() / (xLabels.length);
  298. var xlabelsUL = $('<ul class="visualize-labels-x"></ul>')
  299. .width(canvas.width())
  300. .height(canvas.height())
  301. .insertBefore(canvas);
  302. $.each(xLabels, function(i){
  303. var thisLi = $('<li><span class="label">'+this+'</span></li>')
  304. .prepend('<span class="line" />')
  305. .css('left', xInterval * i)
  306. .width(xInterval)
  307. .appendTo(xlabelsUL);
  308. var label = thisLi.find('span.label');
  309. label.addClass('label');
  310. });
  311. //write Y labels
  312. var yScale = canvas.height() / totalYRange;
  313. var liBottom = canvas.height() / (yLabels.length-1);
  314. var ylabelsUL = $('<ul class="visualize-labels-y"></ul>')
  315. .width(canvas.width())
  316. .height(canvas.height())
  317. .insertBefore(canvas);
  318. $.each(yLabels, function(i){
  319. var thisLi = $('<li><span>'+this+'</span></li>')
  320. .prepend('<span class="line" />')
  321. .css('bottom',liBottom*i)
  322. .prependTo(ylabelsUL);
  323. var label = thisLi.find('span:not(.line)');
  324. var topOffset = label.height()/-2;
  325. if(i == 0){ topOffset = -label.height(); }
  326. else if(i== yLabels.length-1){ topOffset = 0; }
  327. label
  328. .css('margin-top', topOffset)
  329. .addClass('label');
  330. });
  331. //start from the bottom left
  332. ctx.translate(0,zeroLoc);
  333. //iterate and draw
  334. for(var h=0; h<dataGroups.length; h++){
  335. ctx.beginPath();
  336. var linewidth = (xInterval-o.barGroupMargin*2) / dataGroups.length; //removed +1
  337. var strokeWidth = linewidth - (o.barMargin*2);
  338. ctx.lineWidth = strokeWidth;
  339. var points = dataGroups[h].points;
  340. var integer = 0;
  341. for(var i=0; i<points.length; i++){
  342. var xVal = (integer-o.barGroupMargin)+(h*linewidth)+linewidth/2;
  343. xVal += o.barGroupMargin*2;
  344. ctx.moveTo(xVal, 0);
  345. ctx.lineTo(xVal, Math.round(-points[i]*yScale));
  346. integer+=xInterval;
  347. }
  348. ctx.strokeStyle = dataGroups[h].color;
  349. ctx.stroke();
  350. ctx.closePath();
  351. }
  352. }
  353. };
  354. //create new canvas, set w&h attrs (not inline styles)
  355. var canvasNode = document.createElement("canvas");
  356. canvasNode.setAttribute('height',o.height);
  357. canvasNode.setAttribute('width',o.width);
  358. var canvas = $(canvasNode);
  359. //get title for chart
  360. var title = o.title || self.find('caption').text();
  361. //create canvas wrapper div, set inline w&h, append
  362. var canvasContain = (container || $('<div class="visualize" role="img" aria-label="Chart representing data from the table: '+ title +'" />'))
  363. .height(o.height)
  364. .width(o.width)
  365. .append(canvas);
  366. //scrape table (this should be cleaned up into an obj)
  367. var tableData = scrapeTable();
  368. var dataGroups = tableData.dataGroups();
  369. var allData = tableData.allData();
  370. var dataSum = tableData.dataSum();
  371. var topValue = tableData.topValue();
  372. var bottomValue = tableData.bottomValue();
  373. var memberTotals = tableData.memberTotals();
  374. var totalYRange = tableData.totalYRange();
  375. var zeroLoc = o.height * (topValue/totalYRange);
  376. var xLabels = tableData.xLabels();
  377. var yLabels = tableData.yLabels();
  378. //title/key container
  379. if(o.appendTitle || o.appendKey){
  380. var infoContain = $('<div class="visualize-info"></div>')
  381. .appendTo(canvasContain);
  382. }
  383. //append title
  384. if(o.appendTitle){
  385. $('<div class="visualize-title">'+ title +'</div>').appendTo(infoContain);
  386. }
  387. //append key
  388. if(o.appendKey){
  389. var newKey = $('<ul class="visualize-key"></ul>');
  390. var selector;
  391. if(o.parseDirection == 'x'){
  392. selector = self.find('tr:gt(0) th').filter(o.rowFilter);
  393. }
  394. else{
  395. selector = self.find('tr:eq(0) th').filter(o.colFilter);
  396. }
  397. selector.each(function(i){
  398. $('<li><span class="visualize-key-color" style="background: '+dataGroups[i].color+'"></span><span class="visualize-key-label">'+ $(this).text() +'</span></li>')
  399. .appendTo(newKey);
  400. });
  401. newKey.appendTo(infoContain);
  402. };
  403. //append new canvas to page
  404. if(!container){canvasContain.insertAfter(this); }
  405. if( typeof(G_vmlCanvasManager) != 'undefined' ){ G_vmlCanvasManager.init(); G_vmlCanvasManager.initElement(canvas[0]); }
  406. //set up the drawing board
  407. var ctx = canvas[0].getContext('2d');
  408. //create chart
  409. createChart[o.type]();
  410. //clean up some doubled lines that sit on top of canvas borders (done via JS due to IE)
  411. $('.visualize-line li:first-child span.line, .visualize-line li:last-child span.line, .visualize-area li:first-child span.line, .visualize-area li:last-child span.line, .visualize-bar li:first-child span.line,.visualize-bar .visualize-labels-y li:last-child span.line').css('border','none');
  412. if(!container){
  413. //add event for updating
  414. canvasContain.bind('visualizeRefresh', function(){
  415. self.visualize(o, $(this).empty());
  416. });
  417. }
  418. }).next(); //returns canvas(es)
  419. };
  420. })(jQuery);