You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

436 line
18 KiB

  1. /**
  2. * jqPlot
  3. * Pure JavaScript plotting plugin using jQuery
  4. *
  5. * Version: 1.0.0b2_r792
  6. *
  7. * Copyright (c) 2009-2011 Chris Leonello
  8. * jqPlot is currently available for use in all personal or commercial projects
  9. * under both the MIT (http://www.opensource.org/licenses/mit-license.php) and GPL
  10. * version 2.0 (http://www.gnu.org/licenses/gpl-2.0.html) licenses. This means that you can
  11. * choose the license that best suits your project and use it accordingly.
  12. *
  13. * Although not required, the author would appreciate an email letting him
  14. * know of any substantial use of jqPlot. You can reach the author at:
  15. * chris at jqplot dot com or see http://www.jqplot.com/info.php .
  16. *
  17. * If you are feeling kind and generous, consider supporting the project by
  18. * making a donation at: http://www.jqplot.com/donate.php .
  19. *
  20. * sprintf functions contained in jqplot.sprintf.js by Ash Searle:
  21. *
  22. * version 2007.04.27
  23. * author Ash Searle
  24. * http://hexmen.com/blog/2007/03/printf-sprintf/
  25. * http://hexmen.com/js/sprintf.js
  26. * The author (Ash Searle) has placed this code in the public domain:
  27. * "This code is unrestricted: you are free to use it however you like."
  28. *
  29. */
  30. (function($) {
  31. /**
  32. * Class: $.jqplot.MekkoRenderer
  33. * Draws a Mekko style chart which shows 3 dimensional data on a 2 dimensional graph.
  34. * the <$.jqplot.MekkoAxisRenderer> should be used with mekko charts. The mekko renderer
  35. * overrides the default legend renderer with it's own $.jqplot.MekkoLegendRenderer
  36. * which allows more flexibility to specify number of rows and columns in the legend.
  37. *
  38. * Data is specified per bar in the chart. You can specify data as an array of y values, or as
  39. * an array of [label, value] pairs. Note that labels are used only on the first series.
  40. * Labels on subsequent series are ignored:
  41. *
  42. * > bar1 = [['shirts', 8],['hats', 14],['shoes', 6],['gloves', 16],['dolls', 12]];
  43. * > bar2 = [15,6,9,13,6];
  44. * > bar3 = [['grumpy',4],['sneezy',2],['happy',7],['sleepy',9],['doc',7]];
  45. *
  46. * If you want to place labels for each bar under the axis, you use the barLabels option on
  47. * the axes. The bar labels can be styled with the ".jqplot-mekko-barLabel" css class.
  48. *
  49. * > barLabels = ['Mickey Mouse', 'Donald Duck', 'Goofy'];
  50. * > axes:{xaxis:{barLabels:barLabels}}
  51. *
  52. */
  53. $.jqplot.MekkoRenderer = function(){
  54. this.shapeRenderer = new $.jqplot.ShapeRenderer();
  55. // prop: borderColor
  56. // color of the borders between areas on the chart
  57. this.borderColor = null;
  58. // prop: showBorders
  59. // True to draw borders lines between areas on the chart.
  60. // False will draw borders lines with the same color as the area.
  61. this.showBorders = true;
  62. };
  63. // called with scope of series.
  64. $.jqplot.MekkoRenderer.prototype.init = function(options, plot) {
  65. this.fill = false;
  66. this.fillRect = true;
  67. this.strokeRect = true;
  68. this.shadow = false;
  69. // width of bar on x axis.
  70. this._xwidth = 0;
  71. this._xstart = 0;
  72. $.extend(true, this.renderer, options);
  73. // set the shape renderer options
  74. var opts = {lineJoin:'miter', lineCap:'butt', isarc:false, fillRect:this.fillRect, strokeRect:this.strokeRect};
  75. this.renderer.shapeRenderer.init(opts);
  76. plot.axes.x2axis._series.push(this);
  77. this._type = 'mekko';
  78. };
  79. // Method: setGridData
  80. // converts the user data values to grid coordinates and stores them
  81. // in the gridData array. Will convert user data into appropriate
  82. // rectangles.
  83. // Called with scope of a series.
  84. $.jqplot.MekkoRenderer.prototype.setGridData = function(plot) {
  85. // recalculate the grid data
  86. var xp = this._xaxis.series_u2p;
  87. var yp = this._yaxis.series_u2p;
  88. var data = this._plotData;
  89. this.gridData = [];
  90. // figure out width on x axis.
  91. // this._xwidth = this._sumy / plot._sumy * this.canvas.getWidth();
  92. this._xwidth = xp(this._sumy) - xp(0);
  93. if (this.index>0) {
  94. this._xstart = plot.series[this.index-1]._xstart + plot.series[this.index-1]._xwidth;
  95. }
  96. var totheight = this.canvas.getHeight();
  97. var sumy = 0;
  98. var cury;
  99. var curheight;
  100. for (var i=0; i<data.length; i++) {
  101. if (data[i] != null) {
  102. sumy += data[i][1];
  103. cury = totheight - (sumy / this._sumy * totheight);
  104. curheight = data[i][1] / this._sumy * totheight;
  105. this.gridData.push([this._xstart, cury, this._xwidth, curheight]);
  106. }
  107. }
  108. };
  109. // Method: makeGridData
  110. // converts any arbitrary data values to grid coordinates and
  111. // returns them. This method exists so that plugins can use a series'
  112. // linerenderer to generate grid data points without overwriting the
  113. // grid data associated with that series.
  114. // Called with scope of a series.
  115. $.jqplot.MekkoRenderer.prototype.makeGridData = function(data, plot) {
  116. // recalculate the grid data
  117. // figure out width on x axis.
  118. var xp = this._xaxis.series_u2p;
  119. var totheight = this.canvas.getHeight();
  120. var sumy = 0;
  121. var cury;
  122. var curheight;
  123. var gd = [];
  124. for (var i=0; i<data.length; i++) {
  125. if (data[i] != null) {
  126. sumy += data[i][1];
  127. cury = totheight - (sumy / this._sumy * totheight);
  128. curheight = data[i][1] / this._sumy * totheight;
  129. gd.push([this._xstart, cury, this._xwidth, curheight]);
  130. }
  131. }
  132. return gd;
  133. };
  134. // called within scope of series.
  135. $.jqplot.MekkoRenderer.prototype.draw = function(ctx, gd, options) {
  136. var i;
  137. var opts = (options != undefined) ? options : {};
  138. var showLine = (opts.showLine != undefined) ? opts.showLine : this.showLine;
  139. var colorGenerator = new $.jqplot.ColorGenerator(this.seriesColors);
  140. ctx.save();
  141. if (gd.length) {
  142. if (showLine) {
  143. for (i=0; i<gd.length; i++){
  144. opts.fillStyle = colorGenerator.next();
  145. if (this.renderer.showBorders) {
  146. opts.strokeStyle = this.renderer.borderColor;
  147. }
  148. else {
  149. opts.strokeStyle = opts.fillStyle;
  150. }
  151. this.renderer.shapeRenderer.draw(ctx, gd[i], opts);
  152. }
  153. }
  154. }
  155. ctx.restore();
  156. };
  157. $.jqplot.MekkoRenderer.prototype.drawShadow = function(ctx, gd, options) {
  158. // This is a no-op, no shadows on mekko charts.
  159. };
  160. /**
  161. * Class: $.jqplot.MekkoLegendRenderer
  162. * Legend renderer used by mekko charts with options for
  163. * controlling number or rows and columns as well as placement
  164. * outside of plot area.
  165. *
  166. */
  167. $.jqplot.MekkoLegendRenderer = function(){
  168. //
  169. };
  170. $.jqplot.MekkoLegendRenderer.prototype.init = function(options) {
  171. // prop: numberRows
  172. // Maximum number of rows in the legend. 0 or null for unlimited.
  173. this.numberRows = null;
  174. // prop: numberColumns
  175. // Maximum number of columns in the legend. 0 or null for unlimited.
  176. this.numberColumns = null;
  177. // this will override the placement option on the Legend object
  178. this.placement = "outside";
  179. $.extend(true, this, options);
  180. };
  181. // called with scope of legend
  182. $.jqplot.MekkoLegendRenderer.prototype.draw = function() {
  183. var legend = this;
  184. if (this.show) {
  185. var series = this._series;
  186. var ss = 'position:absolute;';
  187. ss += (this.background) ? 'background:'+this.background+';' : '';
  188. ss += (this.border) ? 'border:'+this.border+';' : '';
  189. ss += (this.fontSize) ? 'font-size:'+this.fontSize+';' : '';
  190. ss += (this.fontFamily) ? 'font-family:'+this.fontFamily+';' : '';
  191. ss += (this.textColor) ? 'color:'+this.textColor+';' : '';
  192. this._elem = $('<table class="jqplot-table-legend" style="'+ss+'"></table>');
  193. // Mekko charts legends don't go by number of series, but by number of data points
  194. // in the series. Refactor things here for that.
  195. var pad = false,
  196. reverse = true, // mekko charts are always stacked, so reverse
  197. nr, nc;
  198. var s = series[0];
  199. var colorGenerator = new $.jqplot.ColorGenerator(s.seriesColors);
  200. if (s.show) {
  201. var pd = s.data;
  202. if (this.numberRows) {
  203. nr = this.numberRows;
  204. if (!this.numberColumns){
  205. nc = Math.ceil(pd.length/nr);
  206. }
  207. else{
  208. nc = this.numberColumns;
  209. }
  210. }
  211. else if (this.numberColumns) {
  212. nc = this.numberColumns;
  213. nr = Math.ceil(pd.length/this.numberColumns);
  214. }
  215. else {
  216. nr = pd.length;
  217. nc = 1;
  218. }
  219. var i, j, tr, td1, td2, lt, rs, color;
  220. var idx = 0;
  221. for (i=0; i<nr; i++) {
  222. if (reverse){
  223. tr = $('<tr class="jqplot-table-legend"></tr>').prependTo(this._elem);
  224. }
  225. else{
  226. tr = $('<tr class="jqplot-table-legend"></tr>').appendTo(this._elem);
  227. }
  228. for (j=0; j<nc; j++) {
  229. if (idx < pd.length) {
  230. lt = this.labels[idx] || pd[idx][0].toString();
  231. color = colorGenerator.next();
  232. if (!reverse){
  233. if (i>0){
  234. pad = true;
  235. }
  236. else{
  237. pad = false;
  238. }
  239. }
  240. else{
  241. if (i == nr -1){
  242. pad = false;
  243. }
  244. else{
  245. pad = true;
  246. }
  247. }
  248. rs = (pad) ? this.rowSpacing : '0';
  249. td1 = $('<td class="jqplot-table-legend" style="text-align:center;padding-top:'+rs+';">'+
  250. '<div><div class="jqplot-table-legend-swatch" style="border-color:'+color+';"></div>'+
  251. '</div></td>');
  252. td2 = $('<td class="jqplot-table-legend" style="padding-top:'+rs+';"></td>');
  253. if (this.escapeHtml){
  254. td2.text(lt);
  255. }
  256. else {
  257. td2.html(lt);
  258. }
  259. if (reverse) {
  260. td2.prependTo(tr);
  261. td1.prependTo(tr);
  262. }
  263. else {
  264. td1.appendTo(tr);
  265. td2.appendTo(tr);
  266. }
  267. pad = true;
  268. }
  269. idx++;
  270. }
  271. }
  272. tr = null;
  273. td1 = null;
  274. td2 = null;
  275. }
  276. }
  277. return this._elem;
  278. };
  279. $.jqplot.MekkoLegendRenderer.prototype.pack = function(offsets) {
  280. if (this.show) {
  281. // fake a grid for positioning
  282. var grid = {_top:offsets.top, _left:offsets.left, _right:offsets.right, _bottom:this._plotDimensions.height - offsets.bottom};
  283. if (this.placement == 'insideGrid') {
  284. switch (this.location) {
  285. case 'nw':
  286. var a = grid._left + this.xoffset;
  287. var b = grid._top + this.yoffset;
  288. this._elem.css('left', a);
  289. this._elem.css('top', b);
  290. break;
  291. case 'n':
  292. var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
  293. var b = grid._top + this.yoffset;
  294. this._elem.css('left', a);
  295. this._elem.css('top', b);
  296. break;
  297. case 'ne':
  298. var a = offsets.right + this.xoffset;
  299. var b = grid._top + this.yoffset;
  300. this._elem.css({right:a, top:b});
  301. break;
  302. case 'e':
  303. var a = offsets.right + this.xoffset;
  304. var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
  305. this._elem.css({right:a, top:b});
  306. break;
  307. case 'se':
  308. var a = offsets.right + this.xoffset;
  309. var b = offsets.bottom + this.yoffset;
  310. this._elem.css({right:a, bottom:b});
  311. break;
  312. case 's':
  313. var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
  314. var b = offsets.bottom + this.yoffset;
  315. this._elem.css({left:a, bottom:b});
  316. break;
  317. case 'sw':
  318. var a = grid._left + this.xoffset;
  319. var b = offsets.bottom + this.yoffset;
  320. this._elem.css({left:a, bottom:b});
  321. break;
  322. case 'w':
  323. var a = grid._left + this.xoffset;
  324. var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
  325. this._elem.css({left:a, top:b});
  326. break;
  327. default: // same as 'se'
  328. var a = grid._right - this.xoffset;
  329. var b = grid._bottom + this.yoffset;
  330. this._elem.css({right:a, bottom:b});
  331. break;
  332. }
  333. }
  334. else {
  335. switch (this.location) {
  336. case 'nw':
  337. var a = this._plotDimensions.width - grid._left + this.xoffset;
  338. var b = grid._top + this.yoffset;
  339. this._elem.css('right', a);
  340. this._elem.css('top', b);
  341. break;
  342. case 'n':
  343. var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
  344. var b = this._plotDimensions.height - grid._top + this.yoffset;
  345. this._elem.css('left', a);
  346. this._elem.css('bottom', b);
  347. break;
  348. case 'ne':
  349. var a = this._plotDimensions.width - offsets.right + this.xoffset;
  350. var b = grid._top + this.yoffset;
  351. this._elem.css({left:a, top:b});
  352. break;
  353. case 'e':
  354. var a = this._plotDimensions.width - offsets.right + this.xoffset;
  355. var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
  356. this._elem.css({left:a, top:b});
  357. break;
  358. case 'se':
  359. var a = this._plotDimensions.width - offsets.right + this.xoffset;
  360. var b = offsets.bottom + this.yoffset;
  361. this._elem.css({left:a, bottom:b});
  362. break;
  363. case 's':
  364. var a = (offsets.left + (this._plotDimensions.width - offsets.right))/2 - this.getWidth()/2;
  365. var b = this._plotDimensions.height - offsets.bottom + this.yoffset;
  366. this._elem.css({left:a, top:b});
  367. break;
  368. case 'sw':
  369. var a = this._plotDimensions.width - grid._left + this.xoffset;
  370. var b = offsets.bottom + this.yoffset;
  371. this._elem.css({right:a, bottom:b});
  372. break;
  373. case 'w':
  374. var a = this._plotDimensions.width - grid._left + this.xoffset;
  375. var b = (offsets.top + (this._plotDimensions.height - offsets.bottom))/2 - this.getHeight()/2;
  376. this._elem.css({right:a, top:b});
  377. break;
  378. default: // same as 'se'
  379. var a = grid._right - this.xoffset;
  380. var b = grid._bottom + this.yoffset;
  381. this._elem.css({right:a, bottom:b});
  382. break;
  383. }
  384. }
  385. }
  386. };
  387. // setup default renderers for axes and legend so user doesn't have to
  388. // called with scope of plot
  389. function preInit(target, data, options) {
  390. options = options || {};
  391. options.axesDefaults = options.axesDefaults || {};
  392. options.legend = options.legend || {};
  393. options.seriesDefaults = options.seriesDefaults || {};
  394. var setopts = false;
  395. if (options.seriesDefaults.renderer == $.jqplot.MekkoRenderer) {
  396. setopts = true;
  397. }
  398. else if (options.series) {
  399. for (var i=0; i < options.series.length; i++) {
  400. if (options.series[i].renderer == $.jqplot.MekkoRenderer) {
  401. setopts = true;
  402. }
  403. }
  404. }
  405. if (setopts) {
  406. options.axesDefaults.renderer = $.jqplot.MekkoAxisRenderer;
  407. options.legend.renderer = $.jqplot.MekkoLegendRenderer;
  408. options.legend.preDraw = true;
  409. }
  410. }
  411. $.jqplot.preInitHooks.push(preInit);
  412. })(jQuery);