PHP/JavaScript webapp to analyse spending habits
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.

jquery.flot.js 88KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119
  1. /* Javascript plotting library for jQuery, v. 0.6.
  2. *
  3. * Released under the MIT license by IOLA, December 2007.
  4. *
  5. */
  6. // first an inline dependency, jquery.colorhelpers.js, we inline it here
  7. // for convenience
  8. /* Plugin for jQuery for working with colors.
  9. *
  10. * Version 1.0.
  11. *
  12. * Inspiration from jQuery color animation plugin by John Resig.
  13. *
  14. * Released under the MIT license by Ole Laursen, October 2009.
  15. *
  16. * Examples:
  17. *
  18. * $.color.parse("#fff").scale('rgb', 0.25).add('a', -0.5).toString()
  19. * var c = $.color.extract($("#mydiv"), 'background-color');
  20. * console.log(c.r, c.g, c.b, c.a);
  21. * $.color.make(100, 50, 25, 0.4).toString() // returns "rgba(100,50,25,0.4)"
  22. *
  23. * Note that .scale() and .add() work in-place instead of returning
  24. * new objects.
  25. */
  26. (function(){jQuery.color={};jQuery.color.make=function(E,D,B,C){var F={};F.r=E||0;F.g=D||0;F.b=B||0;F.a=C!=null?C:1;F.add=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]+=H}return F.normalize()};F.scale=function(I,H){for(var G=0;G<I.length;++G){F[I.charAt(G)]*=H}return F.normalize()};F.toString=function(){if(F.a>=1){return"rgb("+[F.r,F.g,F.b].join(",")+")"}else{return"rgba("+[F.r,F.g,F.b,F.a].join(",")+")"}};F.normalize=function(){function G(I,J,H){return J<I?I:(J>H?H:J)}F.r=G(0,parseInt(F.r),255);F.g=G(0,parseInt(F.g),255);F.b=G(0,parseInt(F.b),255);F.a=G(0,F.a,1);return F};F.clone=function(){return jQuery.color.make(F.r,F.b,F.g,F.a)};return F.normalize()};jQuery.color.extract=function(C,B){var D;do{D=C.css(B).toLowerCase();if(D!=""&&D!="transparent"){break}C=C.parent()}while(!jQuery.nodeName(C.get(0),"body"));if(D=="rgba(0, 0, 0, 0)"){D="transparent"}return jQuery.color.parse(D)};jQuery.color.parse=function(E){var D,B=jQuery.color.make;if(D=/rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10))}if(D=/rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseInt(D[1],10),parseInt(D[2],10),parseInt(D[3],10),parseFloat(D[4]))}if(D=/rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55)}if(D=/rgba\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\s*\)/.exec(E)){return B(parseFloat(D[1])*2.55,parseFloat(D[2])*2.55,parseFloat(D[3])*2.55,parseFloat(D[4]))}if(D=/#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(E)){return B(parseInt(D[1],16),parseInt(D[2],16),parseInt(D[3],16))}if(D=/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(E)){return B(parseInt(D[1]+D[1],16),parseInt(D[2]+D[2],16),parseInt(D[3]+D[3],16))}var C=jQuery.trim(E).toLowerCase();if(C=="transparent"){return B(255,255,255,0)}else{D=A[C];return B(D[0],D[1],D[2])}};var A={aqua:[0,255,255],azure:[240,255,255],beige:[245,245,220],black:[0,0,0],blue:[0,0,255],brown:[165,42,42],cyan:[0,255,255],darkblue:[0,0,139],darkcyan:[0,139,139],darkgrey:[169,169,169],darkgreen:[0,100,0],darkkhaki:[189,183,107],darkmagenta:[139,0,139],darkolivegreen:[85,107,47],darkorange:[255,140,0],darkorchid:[153,50,204],darkred:[139,0,0],darksalmon:[233,150,122],darkviolet:[148,0,211],fuchsia:[255,0,255],gold:[255,215,0],green:[0,128,0],indigo:[75,0,130],khaki:[240,230,140],lightblue:[173,216,230],lightcyan:[224,255,255],lightgreen:[144,238,144],lightgrey:[211,211,211],lightpink:[255,182,193],lightyellow:[255,255,224],lime:[0,255,0],magenta:[255,0,255],maroon:[128,0,0],navy:[0,0,128],olive:[128,128,0],orange:[255,165,0],pink:[255,192,203],purple:[128,0,128],violet:[128,0,128],red:[255,0,0],silver:[192,192,192],white:[255,255,255],yellow:[255,255,0]}})();
  27. // the actual Flot code
  28. (function($) {
  29. function Plot(placeholder, data_, options_, plugins) {
  30. // data is on the form:
  31. // [ series1, series2 ... ]
  32. // where series is either just the data as [ [x1, y1], [x2, y2], ... ]
  33. // or { data: [ [x1, y1], [x2, y2], ... ], label: "some label", ... }
  34. var series = [],
  35. options = {
  36. // the color theme used for graphs
  37. colors: ["#edc240", "#afd8f8", "#cb4b4b", "#4da74d", "#9440ed"],
  38. legend: {
  39. show: true,
  40. noColumns: 1, // number of colums in legend table
  41. labelFormatter: null, // fn: string -> string
  42. labelBoxBorderColor: "#ccc", // border color for the little label boxes
  43. container: null, // container (as jQuery object) to put legend in, null means default on top of graph
  44. position: "ne", // position of default legend container within plot
  45. margin: 5, // distance from grid edge to default legend container within plot
  46. backgroundColor: null, // null means auto-detect
  47. backgroundOpacity: 0.85 // set to 0 to avoid background
  48. },
  49. xaxis: {
  50. mode: null, // null or "time"
  51. transform: null, // null or f: number -> number to transform axis
  52. inverseTransform: null, // if transform is set, this should be the inverse function
  53. min: null, // min. value to show, null means set automatically
  54. max: null, // max. value to show, null means set automatically
  55. autoscaleMargin: null, // margin in % to add if auto-setting min/max
  56. ticks: null, // either [1, 3] or [[1, "a"], 3] or (fn: axis info -> ticks) or app. number of ticks for auto-ticks
  57. tickFormatter: null, // fn: number -> string
  58. labelWidth: null, // size of tick labels in pixels
  59. labelHeight: null,
  60. // mode specific options
  61. tickDecimals: null, // no. of decimals, null means auto
  62. tickSize: null, // number or [number, "unit"]
  63. minTickSize: null, // number or [number, "unit"]
  64. monthNames: null, // list of names of months
  65. timeformat: null, // format string to use
  66. twelveHourClock: false // 12 or 24 time in time mode
  67. },
  68. yaxis: {
  69. autoscaleMargin: 0.02
  70. },
  71. x2axis: {
  72. autoscaleMargin: null
  73. },
  74. y2axis: {
  75. autoscaleMargin: 0.02
  76. },
  77. series: {
  78. points: {
  79. show: false,
  80. radius: 3,
  81. lineWidth: 2, // in pixels
  82. fill: true,
  83. fillColor: "#ffffff"
  84. },
  85. lines: {
  86. // we don't put in show: false so we can see
  87. // whether lines were actively disabled
  88. lineWidth: 2, // in pixels
  89. fill: false,
  90. fillColor: null,
  91. steps: false
  92. },
  93. bars: {
  94. show: false,
  95. lineWidth: 2, // in pixels
  96. barWidth: 1, // in units of the x axis
  97. fill: true,
  98. fillColor: null,
  99. align: "left", // or "center"
  100. horizontal: false // when horizontal, left is now top
  101. },
  102. shadowSize: 3
  103. },
  104. grid: {
  105. show: true,
  106. aboveData: false,
  107. color: "#545454", // primary color used for outline and labels
  108. backgroundColor: null, // null for transparent, else color
  109. tickColor: "rgba(0,0,0,0.15)", // color used for the ticks
  110. labelMargin: 5, // in pixels
  111. borderWidth: 2, // in pixels
  112. borderColor: null, // set if different from the grid color
  113. markings: null, // array of ranges or fn: axes -> array of ranges
  114. markingsColor: "#f4f4f4",
  115. markingsLineWidth: 2,
  116. // interactive stuff
  117. clickable: false,
  118. hoverable: false,
  119. autoHighlight: true, // highlight in case mouse is near
  120. mouseActiveRadius: 10 // how far the mouse can be away to activate an item
  121. },
  122. hooks: {}
  123. },
  124. canvas = null, // the canvas for the plot itself
  125. overlay = null, // canvas for interactive stuff on top of plot
  126. eventHolder = null, // jQuery object that events should be bound to
  127. ctx = null, octx = null,
  128. axes = { xaxis: {}, yaxis: {}, x2axis: {}, y2axis: {} },
  129. plotOffset = { left: 0, right: 0, top: 0, bottom: 0},
  130. canvasWidth = 0, canvasHeight = 0,
  131. plotWidth = 0, plotHeight = 0,
  132. hooks = {
  133. processOptions: [],
  134. processRawData: [],
  135. processDatapoints: [],
  136. draw: [],
  137. bindEvents: [],
  138. drawOverlay: []
  139. },
  140. plot = this;
  141. // public functions
  142. plot.setData = setData;
  143. plot.setupGrid = setupGrid;
  144. plot.draw = draw;
  145. plot.getPlaceholder = function() { return placeholder; };
  146. plot.getCanvas = function() { return canvas; };
  147. plot.getPlotOffset = function() { return plotOffset; };
  148. plot.width = function () { return plotWidth; };
  149. plot.height = function () { return plotHeight; };
  150. plot.offset = function () {
  151. var o = eventHolder.offset();
  152. o.left += plotOffset.left;
  153. o.top += plotOffset.top;
  154. return o;
  155. };
  156. plot.getData = function() { return series; };
  157. plot.getAxes = function() { return axes; };
  158. plot.getOptions = function() { return options; };
  159. plot.highlight = highlight;
  160. plot.unhighlight = unhighlight;
  161. plot.triggerRedrawOverlay = triggerRedrawOverlay;
  162. plot.pointOffset = function(point) {
  163. return { left: parseInt(axisSpecToRealAxis(point, "xaxis").p2c(+point.x) + plotOffset.left),
  164. top: parseInt(axisSpecToRealAxis(point, "yaxis").p2c(+point.y) + plotOffset.top) };
  165. };
  166. // public attributes
  167. plot.hooks = hooks;
  168. // initialize
  169. initPlugins(plot);
  170. parseOptions(options_);
  171. constructCanvas();
  172. setData(data_);
  173. setupGrid();
  174. draw();
  175. bindEvents();
  176. function executeHooks(hook, args) {
  177. args = [plot].concat(args);
  178. for (var i = 0; i < hook.length; ++i)
  179. hook[i].apply(this, args);
  180. }
  181. function initPlugins() {
  182. for (var i = 0; i < plugins.length; ++i) {
  183. var p = plugins[i];
  184. p.init(plot);
  185. if (p.options)
  186. $.extend(true, options, p.options);
  187. }
  188. }
  189. function parseOptions(opts) {
  190. $.extend(true, options, opts);
  191. if (options.grid.borderColor == null)
  192. options.grid.borderColor = options.grid.color;
  193. // backwards compatibility, to be removed in future
  194. if (options.xaxis.noTicks && options.xaxis.ticks == null)
  195. options.xaxis.ticks = options.xaxis.noTicks;
  196. if (options.yaxis.noTicks && options.yaxis.ticks == null)
  197. options.yaxis.ticks = options.yaxis.noTicks;
  198. if (options.grid.coloredAreas)
  199. options.grid.markings = options.grid.coloredAreas;
  200. if (options.grid.coloredAreasColor)
  201. options.grid.markingsColor = options.grid.coloredAreasColor;
  202. if (options.lines)
  203. $.extend(true, options.series.lines, options.lines);
  204. if (options.points)
  205. $.extend(true, options.series.points, options.points);
  206. if (options.bars)
  207. $.extend(true, options.series.bars, options.bars);
  208. if (options.shadowSize)
  209. options.series.shadowSize = options.shadowSize;
  210. for (var n in hooks)
  211. if (options.hooks[n] && options.hooks[n].length)
  212. hooks[n] = hooks[n].concat(options.hooks[n]);
  213. executeHooks(hooks.processOptions, [options]);
  214. }
  215. function setData(d) {
  216. series = parseData(d);
  217. fillInSeriesOptions();
  218. processData();
  219. }
  220. function parseData(d) {
  221. var res = [];
  222. for (var i = 0; i < d.length; ++i) {
  223. var s = $.extend(true, {}, options.series);
  224. if (d[i].data) {
  225. s.data = d[i].data; // move the data instead of deep-copy
  226. delete d[i].data;
  227. $.extend(true, s, d[i]);
  228. d[i].data = s.data;
  229. }
  230. else
  231. s.data = d[i];
  232. res.push(s);
  233. }
  234. return res;
  235. }
  236. function axisSpecToRealAxis(obj, attr) {
  237. var a = obj[attr];
  238. if (!a || a == 1)
  239. return axes[attr];
  240. if (typeof a == "number")
  241. return axes[attr.charAt(0) + a + attr.slice(1)];
  242. return a; // assume it's OK
  243. }
  244. function fillInSeriesOptions() {
  245. var i;
  246. // collect what we already got of colors
  247. var neededColors = series.length,
  248. usedColors = [],
  249. assignedColors = [];
  250. for (i = 0; i < series.length; ++i) {
  251. var sc = series[i].color;
  252. if (sc != null) {
  253. --neededColors;
  254. if (typeof sc == "number")
  255. assignedColors.push(sc);
  256. else
  257. usedColors.push($.color.parse(series[i].color));
  258. }
  259. }
  260. // we might need to generate more colors if higher indices
  261. // are assigned
  262. for (i = 0; i < assignedColors.length; ++i) {
  263. neededColors = Math.max(neededColors, assignedColors[i] + 1);
  264. }
  265. // produce colors as needed
  266. var colors = [], variation = 0;
  267. i = 0;
  268. while (colors.length < neededColors) {
  269. var c;
  270. if (options.colors.length == i) // check degenerate case
  271. c = $.color.make(100, 100, 100);
  272. else
  273. c = $.color.parse(options.colors[i]);
  274. // vary color if needed
  275. var sign = variation % 2 == 1 ? -1 : 1;
  276. c.scale('rgb', 1 + sign * Math.ceil(variation / 2) * 0.2)
  277. // FIXME: if we're getting to close to something else,
  278. // we should probably skip this one
  279. colors.push(c);
  280. ++i;
  281. if (i >= options.colors.length) {
  282. i = 0;
  283. ++variation;
  284. }
  285. }
  286. // fill in the options
  287. var colori = 0, s;
  288. for (i = 0; i < series.length; ++i) {
  289. s = series[i];
  290. // assign colors
  291. if (s.color == null) {
  292. s.color = colors[colori].toString();
  293. ++colori;
  294. }
  295. else if (typeof s.color == "number")
  296. s.color = colors[s.color].toString();
  297. // turn on lines automatically in case nothing is set
  298. if (s.lines.show == null) {
  299. var v, show = true;
  300. for (v in s)
  301. if (s[v].show) {
  302. show = false;
  303. break;
  304. }
  305. if (show)
  306. s.lines.show = true;
  307. }
  308. // setup axes
  309. s.xaxis = axisSpecToRealAxis(s, "xaxis");
  310. s.yaxis = axisSpecToRealAxis(s, "yaxis");
  311. }
  312. }
  313. function processData() {
  314. var topSentry = Number.POSITIVE_INFINITY,
  315. bottomSentry = Number.NEGATIVE_INFINITY,
  316. i, j, k, m, length,
  317. s, points, ps, x, y, axis, val, f, p;
  318. for (axis in axes) {
  319. axes[axis].datamin = topSentry;
  320. axes[axis].datamax = bottomSentry;
  321. axes[axis].used = false;
  322. }
  323. function updateAxis(axis, min, max) {
  324. if (min < axis.datamin)
  325. axis.datamin = min;
  326. if (max > axis.datamax)
  327. axis.datamax = max;
  328. }
  329. for (i = 0; i < series.length; ++i) {
  330. s = series[i];
  331. s.datapoints = { points: [] };
  332. executeHooks(hooks.processRawData, [ s, s.data, s.datapoints ]);
  333. }
  334. // first pass: clean and copy data
  335. for (i = 0; i < series.length; ++i) {
  336. s = series[i];
  337. var data = s.data, format = s.datapoints.format;
  338. if (!format) {
  339. format = [];
  340. // find out how to copy
  341. format.push({ x: true, number: true, required: true });
  342. format.push({ y: true, number: true, required: true });
  343. if (s.bars.show)
  344. format.push({ y: true, number: true, required: false, defaultValue: 0 });
  345. s.datapoints.format = format;
  346. }
  347. if (s.datapoints.pointsize != null)
  348. continue; // already filled in
  349. if (s.datapoints.pointsize == null)
  350. s.datapoints.pointsize = format.length;
  351. ps = s.datapoints.pointsize;
  352. points = s.datapoints.points;
  353. insertSteps = s.lines.show && s.lines.steps;
  354. s.xaxis.used = s.yaxis.used = true;
  355. for (j = k = 0; j < data.length; ++j, k += ps) {
  356. p = data[j];
  357. var nullify = p == null;
  358. if (!nullify) {
  359. for (m = 0; m < ps; ++m) {
  360. val = p[m];
  361. f = format[m];
  362. if (f) {
  363. if (f.number && val != null) {
  364. val = +val; // convert to number
  365. if (isNaN(val))
  366. val = null;
  367. }
  368. if (val == null) {
  369. if (f.required)
  370. nullify = true;
  371. if (f.defaultValue != null)
  372. val = f.defaultValue;
  373. }
  374. }
  375. points[k + m] = val;
  376. }
  377. }
  378. if (nullify) {
  379. for (m = 0; m < ps; ++m) {
  380. val = points[k + m];
  381. if (val != null) {
  382. f = format[m];
  383. // extract min/max info
  384. if (f.x)
  385. updateAxis(s.xaxis, val, val);
  386. if (f.y)
  387. updateAxis(s.yaxis, val, val);
  388. }
  389. points[k + m] = null;
  390. }
  391. }
  392. else {
  393. // a little bit of line specific stuff that
  394. // perhaps shouldn't be here, but lacking
  395. // better means...
  396. if (insertSteps && k > 0
  397. && points[k - ps] != null
  398. && points[k - ps] != points[k]
  399. && points[k - ps + 1] != points[k + 1]) {
  400. // copy the point to make room for a middle point
  401. for (m = 0; m < ps; ++m)
  402. points[k + ps + m] = points[k + m];
  403. // middle point has same y
  404. points[k + 1] = points[k - ps + 1];
  405. // we've added a point, better reflect that
  406. k += ps;
  407. }
  408. }
  409. }
  410. }
  411. // give the hooks a chance to run
  412. for (i = 0; i < series.length; ++i) {
  413. s = series[i];
  414. executeHooks(hooks.processDatapoints, [ s, s.datapoints]);
  415. }
  416. // second pass: find datamax/datamin for auto-scaling
  417. for (i = 0; i < series.length; ++i) {
  418. s = series[i];
  419. points = s.datapoints.points,
  420. ps = s.datapoints.pointsize;
  421. var xmin = topSentry, ymin = topSentry,
  422. xmax = bottomSentry, ymax = bottomSentry;
  423. for (j = 0; j < points.length; j += ps) {
  424. if (points[j] == null)
  425. continue;
  426. for (m = 0; m < ps; ++m) {
  427. val = points[j + m];
  428. f = format[m];
  429. if (!f)
  430. continue;
  431. if (f.x) {
  432. if (val < xmin)
  433. xmin = val;
  434. if (val > xmax)
  435. xmax = val;
  436. }
  437. if (f.y) {
  438. if (val < ymin)
  439. ymin = val;
  440. if (val > ymax)
  441. ymax = val;
  442. }
  443. }
  444. }
  445. if (s.bars.show) {
  446. // make sure we got room for the bar on the dancing floor
  447. var delta = s.bars.align == "left" ? 0 : -s.bars.barWidth/2;
  448. if (s.bars.horizontal) {
  449. ymin += delta;
  450. ymax += delta + s.bars.barWidth;
  451. }
  452. else {
  453. xmin += delta;
  454. xmax += delta + s.bars.barWidth;
  455. }
  456. }
  457. updateAxis(s.xaxis, xmin, xmax);
  458. updateAxis(s.yaxis, ymin, ymax);
  459. }
  460. for (axis in axes) {
  461. if (axes[axis].datamin == topSentry)
  462. axes[axis].datamin = null;
  463. if (axes[axis].datamax == bottomSentry)
  464. axes[axis].datamax = null;
  465. }
  466. }
  467. function constructCanvas() {
  468. function makeCanvas(width, height) {
  469. var c = document.createElement('canvas');
  470. c.width = width;
  471. c.height = height;
  472. if ($.browser.msie) // excanvas hack
  473. c = window.G_vmlCanvasManager.initElement(c);
  474. return c;
  475. }
  476. canvasWidth = placeholder.width();
  477. canvasHeight = placeholder.height();
  478. placeholder.html(""); // clear placeholder
  479. if (placeholder.css("position") == 'static')
  480. placeholder.css("position", "relative"); // for positioning labels and overlay
  481. if (canvasWidth <= 0 || canvasHeight <= 0)
  482. throw "Invalid dimensions for plot, width = " + canvasWidth + ", height = " + canvasHeight;
  483. if ($.browser.msie) // excanvas hack
  484. window.G_vmlCanvasManager.init_(document); // make sure everything is setup
  485. // the canvas
  486. canvas = $(makeCanvas(canvasWidth, canvasHeight)).appendTo(placeholder).get(0);
  487. ctx = canvas.getContext("2d");
  488. // overlay canvas for interactive features
  489. overlay = $(makeCanvas(canvasWidth, canvasHeight)).css({ position: 'absolute', left: 0, top: 0 }).appendTo(placeholder).get(0);
  490. octx = overlay.getContext("2d");
  491. octx.stroke();
  492. }
  493. function bindEvents() {
  494. // we include the canvas in the event holder too, because IE 7
  495. // sometimes has trouble with the stacking order
  496. eventHolder = $([overlay, canvas]);
  497. // bind events
  498. if (options.grid.hoverable)
  499. eventHolder.mousemove(onMouseMove);
  500. if (options.grid.clickable)
  501. eventHolder.click(onClick);
  502. executeHooks(hooks.bindEvents, [eventHolder]);
  503. }
  504. function setupGrid() {
  505. function setTransformationHelpers(axis, o) {
  506. function identity(x) { return x; }
  507. var s, m, t = o.transform || identity,
  508. it = o.inverseTransform;
  509. // add transformation helpers
  510. if (axis == axes.xaxis || axis == axes.x2axis) {
  511. // precompute how much the axis is scaling a point
  512. // in canvas space
  513. s = axis.scale = plotWidth / (t(axis.max) - t(axis.min));
  514. m = t(axis.min);
  515. // data point to canvas coordinate
  516. if (t == identity) // slight optimization
  517. axis.p2c = function (p) { return (p - m) * s; };
  518. else
  519. axis.p2c = function (p) { return (t(p) - m) * s; };
  520. // canvas coordinate to data point
  521. if (!it)
  522. axis.c2p = function (c) { return m + c / s; };
  523. else
  524. axis.c2p = function (c) { return it(m + c / s); };
  525. }
  526. else {
  527. s = axis.scale = plotHeight / (t(axis.max) - t(axis.min));
  528. m = t(axis.max);
  529. if (t == identity)
  530. axis.p2c = function (p) { return (m - p) * s; };
  531. else
  532. axis.p2c = function (p) { return (m - t(p)) * s; };
  533. if (!it)
  534. axis.c2p = function (c) { return m - c / s; };
  535. else
  536. axis.c2p = function (c) { return it(m - c / s); };
  537. }
  538. }
  539. function measureLabels(axis, axisOptions) {
  540. var i, labels = [], l;
  541. axis.labelWidth = axisOptions.labelWidth;
  542. axis.labelHeight = axisOptions.labelHeight;
  543. if (axis == axes.xaxis || axis == axes.x2axis) {
  544. // to avoid measuring the widths of the labels, we
  545. // construct fixed-size boxes and put the labels inside
  546. // them, we don't need the exact figures and the
  547. // fixed-size box content is easy to center
  548. if (axis.labelWidth == null)
  549. axis.labelWidth = canvasWidth / (axis.ticks.length > 0 ? axis.ticks.length : 1);
  550. // measure x label heights
  551. if (axis.labelHeight == null) {
  552. labels = [];
  553. for (i = 0; i < axis.ticks.length; ++i) {
  554. l = axis.ticks[i].label;
  555. if (l)
  556. labels.push('<div class="tickLabel" style="float:left;width:' + axis.labelWidth + 'px">' + l + '</div>');
  557. }
  558. if (labels.length > 0) {
  559. var dummyDiv = $('<div style="position:absolute;top:-10000px;width:10000px;font-size:smaller">'
  560. + labels.join("") + '<div style="clear:left"></div></div>').appendTo(placeholder);
  561. axis.labelHeight = dummyDiv.height();
  562. dummyDiv.remove();
  563. }
  564. }
  565. }
  566. else if (axis.labelWidth == null || axis.labelHeight == null) {
  567. // calculate y label dimensions
  568. for (i = 0; i < axis.ticks.length; ++i) {
  569. l = axis.ticks[i].label;
  570. if (l)
  571. labels.push('<div class="tickLabel">' + l + '</div>');
  572. }
  573. if (labels.length > 0) {
  574. var dummyDiv = $('<div style="position:absolute;top:-10000px;font-size:smaller">'
  575. + labels.join("") + '</div>').appendTo(placeholder);
  576. if (axis.labelWidth == null)
  577. axis.labelWidth = dummyDiv.width();
  578. if (axis.labelHeight == null)
  579. axis.labelHeight = dummyDiv.find("div").height();
  580. dummyDiv.remove();
  581. }
  582. }
  583. if (axis.labelWidth == null)
  584. axis.labelWidth = 0;
  585. if (axis.labelHeight == null)
  586. axis.labelHeight = 0;
  587. }
  588. function setGridSpacing() {
  589. // get the most space needed around the grid for things
  590. // that may stick out
  591. var maxOutset = options.grid.borderWidth;
  592. for (i = 0; i < series.length; ++i)
  593. maxOutset = Math.max(maxOutset, 2 * (series[i].points.radius + series[i].points.lineWidth/2));
  594. plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = maxOutset;
  595. var margin = options.grid.labelMargin + options.grid.borderWidth;
  596. if (axes.xaxis.labelHeight > 0)
  597. plotOffset.bottom = Math.max(maxOutset, axes.xaxis.labelHeight + margin);
  598. if (axes.yaxis.labelWidth > 0)
  599. plotOffset.left = Math.max(maxOutset, axes.yaxis.labelWidth + margin);
  600. if (axes.x2axis.labelHeight > 0)
  601. plotOffset.top = Math.max(maxOutset, axes.x2axis.labelHeight + margin);
  602. if (axes.y2axis.labelWidth > 0)
  603. plotOffset.right = Math.max(maxOutset, axes.y2axis.labelWidth + margin);
  604. plotWidth = canvasWidth - plotOffset.left - plotOffset.right;
  605. plotHeight = canvasHeight - plotOffset.bottom - plotOffset.top;
  606. }
  607. var axis;
  608. for (axis in axes)
  609. setRange(axes[axis], options[axis]);
  610. if (options.grid.show) {
  611. for (axis in axes) {
  612. prepareTickGeneration(axes[axis], options[axis]);
  613. setTicks(axes[axis], options[axis]);
  614. measureLabels(axes[axis], options[axis]);
  615. }
  616. setGridSpacing();
  617. }
  618. else {
  619. plotOffset.left = plotOffset.right = plotOffset.top = plotOffset.bottom = 0;
  620. plotWidth = canvasWidth;
  621. plotHeight = canvasHeight;
  622. }
  623. for (axis in axes)
  624. setTransformationHelpers(axes[axis], options[axis]);
  625. if (options.grid.show)
  626. insertLabels();
  627. insertLegend();
  628. }
  629. function setRange(axis, axisOptions) {
  630. var min = +(axisOptions.min != null ? axisOptions.min : axis.datamin),
  631. max = +(axisOptions.max != null ? axisOptions.max : axis.datamax),
  632. delta = max - min;
  633. if (delta == 0.0) {
  634. // degenerate case
  635. var widen = max == 0 ? 1 : 0.01;
  636. if (axisOptions.min == null)
  637. min -= widen;
  638. // alway widen max if we couldn't widen min to ensure we
  639. // don't fall into min == max which doesn't work
  640. if (axisOptions.max == null || axisOptions.min != null)
  641. max += widen;
  642. }
  643. else {
  644. // consider autoscaling
  645. var margin = axisOptions.autoscaleMargin;
  646. if (margin != null) {
  647. if (axisOptions.min == null) {
  648. min -= delta * margin;
  649. // make sure we don't go below zero if all values
  650. // are positive
  651. if (min < 0 && axis.datamin != null && axis.datamin >= 0)
  652. min = 0;
  653. }
  654. if (axisOptions.max == null) {
  655. max += delta * margin;
  656. if (max > 0 && axis.datamax != null && axis.datamax <= 0)
  657. max = 0;
  658. }
  659. }
  660. }
  661. axis.min = min;
  662. axis.max = max;
  663. }
  664. function prepareTickGeneration(axis, axisOptions) {
  665. // estimate number of ticks
  666. var noTicks;
  667. if (typeof axisOptions.ticks == "number" && axisOptions.ticks > 0)
  668. noTicks = axisOptions.ticks;
  669. else if (axis == axes.xaxis || axis == axes.x2axis)
  670. // heuristic based on the model a*sqrt(x) fitted to
  671. // some reasonable data points
  672. noTicks = 0.3 * Math.sqrt(canvasWidth);
  673. else
  674. noTicks = 0.3 * Math.sqrt(canvasHeight);
  675. var delta = (axis.max - axis.min) / noTicks,
  676. size, generator, unit, formatter, i, magn, norm;
  677. if (axisOptions.mode == "time") {
  678. // pretty handling of time
  679. // map of app. size of time units in milliseconds
  680. var timeUnitSize = {
  681. "second": 1000,
  682. "minute": 60 * 1000,
  683. "hour": 60 * 60 * 1000,
  684. "day": 24 * 60 * 60 * 1000,
  685. "month": 30 * 24 * 60 * 60 * 1000,
  686. "year": 365.2425 * 24 * 60 * 60 * 1000
  687. };
  688. // the allowed tick sizes, after 1 year we use
  689. // an integer algorithm
  690. var spec = [
  691. [1, "second"], [2, "second"], [5, "second"], [10, "second"],
  692. [30, "second"],
  693. [1, "minute"], [2, "minute"], [5, "minute"], [10, "minute"],
  694. [30, "minute"],
  695. [1, "hour"], [2, "hour"], [4, "hour"],
  696. [8, "hour"], [12, "hour"],
  697. [1, "day"], [2, "day"], [3, "day"],
  698. [0.25, "month"], [0.5, "month"], [1, "month"],
  699. [2, "month"], [3, "month"], [6, "month"],
  700. [1, "year"]
  701. ];
  702. var minSize = 0;
  703. if (axisOptions.minTickSize != null) {
  704. if (typeof axisOptions.tickSize == "number")
  705. minSize = axisOptions.tickSize;
  706. else
  707. minSize = axisOptions.minTickSize[0] * timeUnitSize[axisOptions.minTickSize[1]];
  708. }
  709. for (i = 0; i < spec.length - 1; ++i)
  710. if (delta < (spec[i][0] * timeUnitSize[spec[i][1]]
  711. + spec[i + 1][0] * timeUnitSize[spec[i + 1][1]]) / 2
  712. && spec[i][0] * timeUnitSize[spec[i][1]] >= minSize)
  713. break;
  714. size = spec[i][0];
  715. unit = spec[i][1];
  716. // special-case the possibility of several years
  717. if (unit == "year") {
  718. magn = Math.pow(10, Math.floor(Math.log(delta / timeUnitSize.year) / Math.LN10));
  719. norm = (delta / timeUnitSize.year) / magn;
  720. if (norm < 1.5)
  721. size = 1;
  722. else if (norm < 3)
  723. size = 2;
  724. else if (norm < 7.5)
  725. size = 5;
  726. else
  727. size = 10;
  728. size *= magn;
  729. }
  730. if (axisOptions.tickSize) {
  731. size = axisOptions.tickSize[0];
  732. unit = axisOptions.tickSize[1];
  733. }
  734. generator = function(axis) {
  735. var ticks = [],
  736. tickSize = axis.tickSize[0], unit = axis.tickSize[1],
  737. d = new Date(axis.min);
  738. var step = tickSize * timeUnitSize[unit];
  739. if (unit == "second")
  740. d.setUTCSeconds(floorInBase(d.getUTCSeconds(), tickSize));
  741. if (unit == "minute")
  742. d.setUTCMinutes(floorInBase(d.getUTCMinutes(), tickSize));
  743. if (unit == "hour")
  744. d.setUTCHours(floorInBase(d.getUTCHours(), tickSize));
  745. if (unit == "month")
  746. d.setUTCMonth(floorInBase(d.getUTCMonth(), tickSize));
  747. if (unit == "year")
  748. d.setUTCFullYear(floorInBase(d.getUTCFullYear(), tickSize));
  749. // reset smaller components
  750. d.setUTCMilliseconds(0);
  751. if (step >= timeUnitSize.minute)
  752. d.setUTCSeconds(0);
  753. if (step >= timeUnitSize.hour)
  754. d.setUTCMinutes(0);
  755. if (step >= timeUnitSize.day)
  756. d.setUTCHours(0);
  757. if (step >= timeUnitSize.day * 4)
  758. d.setUTCDate(1);
  759. if (step >= timeUnitSize.year)
  760. d.setUTCMonth(0);
  761. var carry = 0, v = Number.NaN, prev;
  762. do {
  763. prev = v;
  764. v = d.getTime();
  765. ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
  766. if (unit == "month") {
  767. if (tickSize < 1) {
  768. // a bit complicated - we'll divide the month
  769. // up but we need to take care of fractions
  770. // so we don't end up in the middle of a day
  771. d.setUTCDate(1);
  772. var start = d.getTime();
  773. d.setUTCMonth(d.getUTCMonth() + 1);
  774. var end = d.getTime();
  775. d.setTime(v + carry * timeUnitSize.hour + (end - start) * tickSize);
  776. carry = d.getUTCHours();
  777. d.setUTCHours(0);
  778. }
  779. else
  780. d.setUTCMonth(d.getUTCMonth() + tickSize);
  781. }
  782. else if (unit == "year") {
  783. d.setUTCFullYear(d.getUTCFullYear() + tickSize);
  784. }
  785. else
  786. d.setTime(v + step);
  787. } while (v < axis.max && v != prev);
  788. return ticks;
  789. };
  790. formatter = function (v, axis) {
  791. var d = new Date(v);
  792. // first check global format
  793. if (axisOptions.timeformat != null)
  794. return $.plot.formatDate(d, axisOptions.timeformat, axisOptions.monthNames);
  795. var t = axis.tickSize[0] * timeUnitSize[axis.tickSize[1]];
  796. var span = axis.max - axis.min;
  797. var suffix = (axisOptions.twelveHourClock) ? " %p" : "";
  798. if (t < timeUnitSize.minute)
  799. fmt = "%h:%M:%S" + suffix;
  800. else if (t < timeUnitSize.day) {
  801. if (span < 2 * timeUnitSize.day)
  802. fmt = "%h:%M" + suffix;
  803. else
  804. fmt = "%b %d %h:%M" + suffix;
  805. }
  806. else if (t < timeUnitSize.month)
  807. fmt = "%b %d";
  808. else if (t < timeUnitSize.year) {
  809. if (span < timeUnitSize.year)
  810. fmt = "%b";
  811. else
  812. fmt = "%b %y";
  813. }
  814. else
  815. fmt = "%y";
  816. return $.plot.formatDate(d, fmt, axisOptions.monthNames);
  817. };
  818. }
  819. else {
  820. // pretty rounding of base-10 numbers
  821. var maxDec = axisOptions.tickDecimals;
  822. var dec = -Math.floor(Math.log(delta) / Math.LN10);
  823. if (maxDec != null && dec > maxDec)
  824. dec = maxDec;
  825. magn = Math.pow(10, -dec);
  826. norm = delta / magn; // norm is between 1.0 and 10.0
  827. if (norm < 1.5)
  828. size = 1;
  829. else if (norm < 3) {
  830. size = 2;
  831. // special case for 2.5, requires an extra decimal
  832. if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) {
  833. size = 2.5;
  834. ++dec;
  835. }
  836. }
  837. else if (norm < 7.5)
  838. size = 5;
  839. else
  840. size = 10;
  841. size *= magn;
  842. if (axisOptions.minTickSize != null && size < axisOptions.minTickSize)
  843. size = axisOptions.minTickSize;
  844. if (axisOptions.tickSize != null)
  845. size = axisOptions.tickSize;
  846. axis.tickDecimals = Math.max(0, (maxDec != null) ? maxDec : dec);
  847. generator = function (axis) {
  848. var ticks = [];
  849. // spew out all possible ticks
  850. var start = floorInBase(axis.min, axis.tickSize),
  851. i = 0, v = Number.NaN, prev;
  852. do {
  853. prev = v;
  854. v = start + i * axis.tickSize;
  855. ticks.push({ v: v, label: axis.tickFormatter(v, axis) });
  856. ++i;
  857. } while (v < axis.max && v != prev);
  858. return ticks;
  859. };
  860. formatter = function (v, axis) {
  861. return v.toFixed(axis.tickDecimals);
  862. };
  863. }
  864. axis.tickSize = unit ? [size, unit] : size;
  865. axis.tickGenerator = generator;
  866. if ($.isFunction(axisOptions.tickFormatter))
  867. axis.tickFormatter = function (v, axis) { return "" + axisOptions.tickFormatter(v, axis); };
  868. else
  869. axis.tickFormatter = formatter;
  870. }
  871. function setTicks(axis, axisOptions) {
  872. axis.ticks = [];
  873. if (!axis.used)
  874. return;
  875. if (axisOptions.ticks == null)
  876. axis.ticks = axis.tickGenerator(axis);
  877. else if (typeof axisOptions.ticks == "number") {
  878. if (axisOptions.ticks > 0)
  879. axis.ticks = axis.tickGenerator(axis);
  880. }
  881. else if (axisOptions.ticks) {
  882. var ticks = axisOptions.ticks;
  883. if ($.isFunction(ticks))
  884. // generate the ticks
  885. ticks = ticks({ min: axis.min, max: axis.max });
  886. // clean up the user-supplied ticks, copy them over
  887. var i, v;
  888. for (i = 0; i < ticks.length; ++i) {
  889. var label = null;
  890. var t = ticks[i];
  891. if (typeof t == "object") {
  892. v = t[0];
  893. if (t.length > 1)
  894. label = t[1];
  895. }
  896. else
  897. v = t;
  898. if (label == null)
  899. label = axis.tickFormatter(v, axis);
  900. axis.ticks[i] = { v: v, label: label };
  901. }
  902. }
  903. if (axisOptions.autoscaleMargin != null && axis.ticks.length > 0) {
  904. // snap to ticks
  905. if (axisOptions.min == null)
  906. axis.min = Math.min(axis.min, axis.ticks[0].v);
  907. if (axisOptions.max == null && axis.ticks.length > 1)
  908. axis.max = Math.max(axis.max, axis.ticks[axis.ticks.length - 1].v);
  909. }
  910. }
  911. function draw() {
  912. ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  913. var grid = options.grid;
  914. if (grid.show && !grid.aboveData)
  915. drawGrid();
  916. for (var i = 0; i < series.length; ++i)
  917. drawSeries(series[i]);
  918. executeHooks(hooks.draw, [ctx]);
  919. if (grid.show && grid.aboveData)
  920. drawGrid();
  921. }
  922. function extractRange(ranges, coord) {
  923. var firstAxis = coord + "axis",
  924. secondaryAxis = coord + "2axis",
  925. axis, from, to, reverse;
  926. if (ranges[firstAxis]) {
  927. axis = axes[firstAxis];
  928. from = ranges[firstAxis].from;
  929. to = ranges[firstAxis].to;
  930. }
  931. else if (ranges[secondaryAxis]) {
  932. axis = axes[secondaryAxis];
  933. from = ranges[secondaryAxis].from;
  934. to = ranges[secondaryAxis].to;
  935. }
  936. else {
  937. // backwards-compat stuff - to be removed in future
  938. axis = axes[firstAxis];
  939. from = ranges[coord + "1"];
  940. to = ranges[coord + "2"];
  941. }
  942. // auto-reverse as an added bonus
  943. if (from != null && to != null && from > to)
  944. return { from: to, to: from, axis: axis };
  945. return { from: from, to: to, axis: axis };
  946. }
  947. function drawGrid() {
  948. var i;
  949. ctx.save();
  950. ctx.translate(plotOffset.left, plotOffset.top);
  951. // draw background, if any
  952. if (options.grid.backgroundColor) {
  953. ctx.fillStyle = getColorOrGradient(options.grid.backgroundColor, plotHeight, 0, "rgba(255, 255, 255, 0)");
  954. ctx.fillRect(0, 0, plotWidth, plotHeight);
  955. }
  956. // draw markings
  957. var markings = options.grid.markings;
  958. if (markings) {
  959. if ($.isFunction(markings))
  960. // xmin etc. are backwards-compatible, to be removed in future
  961. markings = markings({ xmin: axes.xaxis.min, xmax: axes.xaxis.max, ymin: axes.yaxis.min, ymax: axes.yaxis.max, xaxis: axes.xaxis, yaxis: axes.yaxis, x2axis: axes.x2axis, y2axis: axes.y2axis });
  962. for (i = 0; i < markings.length; ++i) {
  963. var m = markings[i],
  964. xrange = extractRange(m, "x"),
  965. yrange = extractRange(m, "y");
  966. // fill in missing
  967. if (xrange.from == null)
  968. xrange.from = xrange.axis.min;
  969. if (xrange.to == null)
  970. xrange.to = xrange.axis.max;
  971. if (yrange.from == null)
  972. yrange.from = yrange.axis.min;
  973. if (yrange.to == null)
  974. yrange.to = yrange.axis.max;
  975. // clip
  976. if (xrange.to < xrange.axis.min || xrange.from > xrange.axis.max ||
  977. yrange.to < yrange.axis.min || yrange.from > yrange.axis.max)
  978. continue;
  979. xrange.from = Math.max(xrange.from, xrange.axis.min);
  980. xrange.to = Math.min(xrange.to, xrange.axis.max);
  981. yrange.from = Math.max(yrange.from, yrange.axis.min);
  982. yrange.to = Math.min(yrange.to, yrange.axis.max);
  983. if (xrange.from == xrange.to && yrange.from == yrange.to)
  984. continue;
  985. // then draw
  986. xrange.from = xrange.axis.p2c(xrange.from);
  987. xrange.to = xrange.axis.p2c(xrange.to);
  988. yrange.from = yrange.axis.p2c(yrange.from);
  989. yrange.to = yrange.axis.p2c(yrange.to);
  990. if (xrange.from == xrange.to || yrange.from == yrange.to) {
  991. // draw line
  992. ctx.beginPath();
  993. ctx.strokeStyle = m.color || options.grid.markingsColor;
  994. ctx.lineWidth = m.lineWidth || options.grid.markingsLineWidth;
  995. //ctx.moveTo(Math.floor(xrange.from), yrange.from);
  996. //ctx.lineTo(Math.floor(xrange.to), yrange.to);
  997. ctx.moveTo(xrange.from, yrange.from);
  998. ctx.lineTo(xrange.to, yrange.to);
  999. ctx.stroke();
  1000. }
  1001. else {
  1002. // fill area
  1003. ctx.fillStyle = m.color || options.grid.markingsColor;
  1004. ctx.fillRect(xrange.from, yrange.to,
  1005. xrange.to - xrange.from,
  1006. yrange.from - yrange.to);
  1007. }
  1008. }
  1009. }
  1010. // draw the inner grid
  1011. ctx.lineWidth = 1;
  1012. ctx.strokeStyle = options.grid.tickColor;
  1013. ctx.beginPath();
  1014. var v, axis = axes.xaxis;
  1015. for (i = 0; i < axis.ticks.length; ++i) {
  1016. v = axis.ticks[i].v;
  1017. if (v <= axis.min || v >= axes.xaxis.max)
  1018. continue; // skip those lying on the axes
  1019. ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 0);
  1020. ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, plotHeight);
  1021. }
  1022. axis = axes.yaxis;
  1023. for (i = 0; i < axis.ticks.length; ++i) {
  1024. v = axis.ticks[i].v;
  1025. if (v <= axis.min || v >= axis.max)
  1026. continue;
  1027. ctx.moveTo(0, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
  1028. ctx.lineTo(plotWidth, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
  1029. }
  1030. axis = axes.x2axis;
  1031. for (i = 0; i < axis.ticks.length; ++i) {
  1032. v = axis.ticks[i].v;
  1033. if (v <= axis.min || v >= axis.max)
  1034. continue;
  1035. ctx.moveTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, -5);
  1036. ctx.lineTo(Math.floor(axis.p2c(v)) + ctx.lineWidth/2, 5);
  1037. }
  1038. axis = axes.y2axis;
  1039. for (i = 0; i < axis.ticks.length; ++i) {
  1040. v = axis.ticks[i].v;
  1041. if (v <= axis.min || v >= axis.max)
  1042. continue;
  1043. ctx.moveTo(plotWidth-5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
  1044. ctx.lineTo(plotWidth+5, Math.floor(axis.p2c(v)) + ctx.lineWidth/2);
  1045. }
  1046. ctx.stroke();
  1047. if (options.grid.borderWidth) {
  1048. // draw border
  1049. var bw = options.grid.borderWidth;
  1050. ctx.lineWidth = bw;
  1051. ctx.strokeStyle = options.grid.borderColor;
  1052. ctx.strokeRect(-bw/2, -bw/2, plotWidth + bw, plotHeight + bw);
  1053. }
  1054. ctx.restore();
  1055. }
  1056. function insertLabels() {
  1057. placeholder.find(".tickLabels").remove();
  1058. var html = ['<div class="tickLabels" style="font-size:smaller;color:' + options.grid.color + '">'];
  1059. function addLabels(axis, labelGenerator) {
  1060. for (var i = 0; i < axis.ticks.length; ++i) {
  1061. var tick = axis.ticks[i];
  1062. if (!tick.label || tick.v < axis.min || tick.v > axis.max)
  1063. continue;
  1064. html.push(labelGenerator(tick, axis));
  1065. }
  1066. }
  1067. var margin = options.grid.labelMargin + options.grid.borderWidth;
  1068. addLabels(axes.xaxis, function (tick, axis) {
  1069. return '<div style="position:absolute;top:' + (plotOffset.top + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
  1070. });
  1071. addLabels(axes.yaxis, function (tick, axis) {
  1072. return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;right:' + (plotOffset.right + plotWidth + margin) + 'px;width:' + axis.labelWidth + 'px;text-align:right" class="tickLabel">' + tick.label + "</div>";
  1073. });
  1074. addLabels(axes.x2axis, function (tick, axis) {
  1075. return '<div style="position:absolute;bottom:' + (plotOffset.bottom + plotHeight + margin) + 'px;left:' + Math.round(plotOffset.left + axis.p2c(tick.v) - axis.labelWidth/2) + 'px;width:' + axis.labelWidth + 'px;text-align:center" class="tickLabel">' + tick.label + "</div>";
  1076. });
  1077. addLabels(axes.y2axis, function (tick, axis) {
  1078. return '<div style="position:absolute;top:' + Math.round(plotOffset.top + axis.p2c(tick.v) - axis.labelHeight/2) + 'px;left:' + (plotOffset.left + plotWidth + margin) +'px;width:' + axis.labelWidth + 'px;text-align:left" class="tickLabel">' + tick.label + "</div>";
  1079. });
  1080. html.push('</div>');
  1081. placeholder.append(html.join(""));
  1082. }
  1083. function drawSeries(series) {
  1084. if (series.lines.show)
  1085. drawSeriesLines(series);
  1086. if (series.bars.show)
  1087. drawSeriesBars(series);
  1088. if (series.points.show)
  1089. drawSeriesPoints(series);
  1090. }
  1091. function drawSeriesLines(series) {
  1092. function plotLine(datapoints, xoffset, yoffset, axisx, axisy) {
  1093. var points = datapoints.points,
  1094. ps = datapoints.pointsize,
  1095. prevx = null, prevy = null;
  1096. ctx.beginPath();
  1097. for (var i = ps; i < points.length; i += ps) {
  1098. var x1 = points[i - ps], y1 = points[i - ps + 1],
  1099. x2 = points[i], y2 = points[i + 1];
  1100. if (x1 == null || x2 == null)
  1101. continue;
  1102. // clip with ymin
  1103. if (y1 <= y2 && y1 < axisy.min) {
  1104. if (y2 < axisy.min)
  1105. continue; // line segment is outside
  1106. // compute new intersection point
  1107. x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  1108. y1 = axisy.min;
  1109. }
  1110. else if (y2 <= y1 && y2 < axisy.min) {
  1111. if (y1 < axisy.min)
  1112. continue;
  1113. x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  1114. y2 = axisy.min;
  1115. }
  1116. // clip with ymax
  1117. if (y1 >= y2 && y1 > axisy.max) {
  1118. if (y2 > axisy.max)
  1119. continue;
  1120. x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  1121. y1 = axisy.max;
  1122. }
  1123. else if (y2 >= y1 && y2 > axisy.max) {
  1124. if (y1 > axisy.max)
  1125. continue;
  1126. x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  1127. y2 = axisy.max;
  1128. }
  1129. // clip with xmin
  1130. if (x1 <= x2 && x1 < axisx.min) {
  1131. if (x2 < axisx.min)
  1132. continue;
  1133. y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  1134. x1 = axisx.min;
  1135. }
  1136. else if (x2 <= x1 && x2 < axisx.min) {
  1137. if (x1 < axisx.min)
  1138. continue;
  1139. y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  1140. x2 = axisx.min;
  1141. }
  1142. // clip with xmax
  1143. if (x1 >= x2 && x1 > axisx.max) {
  1144. if (x2 > axisx.max)
  1145. continue;
  1146. y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  1147. x1 = axisx.max;
  1148. }
  1149. else if (x2 >= x1 && x2 > axisx.max) {
  1150. if (x1 > axisx.max)
  1151. continue;
  1152. y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  1153. x2 = axisx.max;
  1154. }
  1155. if (x1 != prevx || y1 != prevy)
  1156. ctx.moveTo(axisx.p2c(x1) + xoffset, axisy.p2c(y1) + yoffset);
  1157. prevx = x2;
  1158. prevy = y2;
  1159. ctx.lineTo(axisx.p2c(x2) + xoffset, axisy.p2c(y2) + yoffset);
  1160. }
  1161. ctx.stroke();
  1162. }
  1163. function plotLineArea(datapoints, axisx, axisy) {
  1164. var points = datapoints.points,
  1165. ps = datapoints.pointsize,
  1166. bottom = Math.min(Math.max(0, axisy.min), axisy.max),
  1167. top, lastX = 0, areaOpen = false;
  1168. for (var i = ps; i < points.length; i += ps) {
  1169. var x1 = points[i - ps], y1 = points[i - ps + 1],
  1170. x2 = points[i], y2 = points[i + 1];
  1171. if (areaOpen && x1 != null && x2 == null) {
  1172. // close area
  1173. ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
  1174. ctx.fill();
  1175. areaOpen = false;
  1176. continue;
  1177. }
  1178. if (x1 == null || x2 == null)
  1179. continue;
  1180. // clip x values
  1181. // clip with xmin
  1182. if (x1 <= x2 && x1 < axisx.min) {
  1183. if (x2 < axisx.min)
  1184. continue;
  1185. y1 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  1186. x1 = axisx.min;
  1187. }
  1188. else if (x2 <= x1 && x2 < axisx.min) {
  1189. if (x1 < axisx.min)
  1190. continue;
  1191. y2 = (axisx.min - x1) / (x2 - x1) * (y2 - y1) + y1;
  1192. x2 = axisx.min;
  1193. }
  1194. // clip with xmax
  1195. if (x1 >= x2 && x1 > axisx.max) {
  1196. if (x2 > axisx.max)
  1197. continue;
  1198. y1 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  1199. x1 = axisx.max;
  1200. }
  1201. else if (x2 >= x1 && x2 > axisx.max) {
  1202. if (x1 > axisx.max)
  1203. continue;
  1204. y2 = (axisx.max - x1) / (x2 - x1) * (y2 - y1) + y1;
  1205. x2 = axisx.max;
  1206. }
  1207. if (!areaOpen) {
  1208. // open area
  1209. ctx.beginPath();
  1210. ctx.moveTo(axisx.p2c(x1), axisy.p2c(bottom));
  1211. areaOpen = true;
  1212. }
  1213. // now first check the case where both is outside
  1214. if (y1 >= axisy.max && y2 >= axisy.max) {
  1215. ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.max));
  1216. ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.max));
  1217. lastX = x2;
  1218. continue;
  1219. }
  1220. else if (y1 <= axisy.min && y2 <= axisy.min) {
  1221. ctx.lineTo(axisx.p2c(x1), axisy.p2c(axisy.min));
  1222. ctx.lineTo(axisx.p2c(x2), axisy.p2c(axisy.min));
  1223. lastX = x2;
  1224. continue;
  1225. }
  1226. // else it's a bit more complicated, there might
  1227. // be two rectangles and two triangles we need to fill
  1228. // in; to find these keep track of the current x values
  1229. var x1old = x1, x2old = x2;
  1230. // and clip the y values, without shortcutting
  1231. // clip with ymin
  1232. if (y1 <= y2 && y1 < axisy.min && y2 >= axisy.min) {
  1233. x1 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  1234. y1 = axisy.min;
  1235. }
  1236. else if (y2 <= y1 && y2 < axisy.min && y1 >= axisy.min) {
  1237. x2 = (axisy.min - y1) / (y2 - y1) * (x2 - x1) + x1;
  1238. y2 = axisy.min;
  1239. }
  1240. // clip with ymax
  1241. if (y1 >= y2 && y1 > axisy.max && y2 <= axisy.max) {
  1242. x1 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  1243. y1 = axisy.max;
  1244. }
  1245. else if (y2 >= y1 && y2 > axisy.max && y1 <= axisy.max) {
  1246. x2 = (axisy.max - y1) / (y2 - y1) * (x2 - x1) + x1;
  1247. y2 = axisy.max;
  1248. }
  1249. // if the x value was changed we got a rectangle
  1250. // to fill
  1251. if (x1 != x1old) {
  1252. if (y1 <= axisy.min)
  1253. top = axisy.min;
  1254. else
  1255. top = axisy.max;
  1256. ctx.lineTo(axisx.p2c(x1old), axisy.p2c(top));
  1257. ctx.lineTo(axisx.p2c(x1), axisy.p2c(top));
  1258. }
  1259. // fill the triangles
  1260. ctx.lineTo(axisx.p2c(x1), axisy.p2c(y1));
  1261. ctx.lineTo(axisx.p2c(x2), axisy.p2c(y2));
  1262. // fill the other rectangle if it's there
  1263. if (x2 != x2old) {
  1264. if (y2 <= axisy.min)
  1265. top = axisy.min;
  1266. else
  1267. top = axisy.max;
  1268. ctx.lineTo(axisx.p2c(x2), axisy.p2c(top));
  1269. ctx.lineTo(axisx.p2c(x2old), axisy.p2c(top));
  1270. }
  1271. lastX = Math.max(x2, x2old);
  1272. }
  1273. if (areaOpen) {
  1274. ctx.lineTo(axisx.p2c(lastX), axisy.p2c(bottom));
  1275. ctx.fill();
  1276. }
  1277. }
  1278. ctx.save();
  1279. ctx.translate(plotOffset.left, plotOffset.top);
  1280. ctx.lineJoin = "round";
  1281. var lw = series.lines.lineWidth,
  1282. sw = series.shadowSize;
  1283. // FIXME: consider another form of shadow when filling is turned on
  1284. if (lw > 0 && sw > 0) {
  1285. // draw shadow as a thick and thin line with transparency
  1286. ctx.lineWidth = sw;
  1287. ctx.strokeStyle = "rgba(0,0,0,0.1)";
  1288. // position shadow at angle from the mid of line
  1289. var angle = Math.PI/18;
  1290. plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/2), Math.cos(angle) * (lw/2 + sw/2), series.xaxis, series.yaxis);
  1291. ctx.lineWidth = sw/2;
  1292. plotLine(series.datapoints, Math.sin(angle) * (lw/2 + sw/4), Math.cos(angle) * (lw/2 + sw/4), series.xaxis, series.yaxis);
  1293. }
  1294. ctx.lineWidth = lw;
  1295. ctx.strokeStyle = series.color;
  1296. var fillStyle = getFillStyle(series.lines, series.color, 0, plotHeight);
  1297. if (fillStyle) {
  1298. ctx.fillStyle = fillStyle;
  1299. plotLineArea(series.datapoints, series.xaxis, series.yaxis);
  1300. }
  1301. if (lw > 0)
  1302. plotLine(series.datapoints, 0, 0, series.xaxis, series.yaxis);
  1303. ctx.restore();
  1304. }
  1305. function drawSeriesPoints(series) {
  1306. function plotPoints(datapoints, radius, fillStyle, offset, circumference, axisx, axisy) {
  1307. var points = datapoints.points, ps = datapoints.pointsize;
  1308. for (var i = 0; i < points.length; i += ps) {
  1309. var x = points[i], y = points[i + 1];
  1310. if (x == null || x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
  1311. continue;
  1312. ctx.beginPath();
  1313. ctx.arc(axisx.p2c(x), axisy.p2c(y) + offset, radius, 0, circumference, false);
  1314. if (fillStyle) {
  1315. ctx.fillStyle = fillStyle;
  1316. ctx.fill();
  1317. }
  1318. ctx.stroke();
  1319. }
  1320. }
  1321. ctx.save();
  1322. ctx.translate(plotOffset.left, plotOffset.top);
  1323. var lw = series.lines.lineWidth,
  1324. sw = series.shadowSize,
  1325. radius = series.points.radius;
  1326. if (lw > 0 && sw > 0) {
  1327. // draw shadow in two steps
  1328. var w = sw / 2;
  1329. ctx.lineWidth = w;
  1330. ctx.strokeStyle = "rgba(0,0,0,0.1)";
  1331. plotPoints(series.datapoints, radius, null, w + w/2, Math.PI,
  1332. series.xaxis, series.yaxis);
  1333. ctx.strokeStyle = "rgba(0,0,0,0.2)";
  1334. plotPoints(series.datapoints, radius, null, w/2, Math.PI,
  1335. series.xaxis, series.yaxis);
  1336. }
  1337. ctx.lineWidth = lw;
  1338. ctx.strokeStyle = series.color;
  1339. plotPoints(series.datapoints, radius,
  1340. getFillStyle(series.points, series.color), 0, 2 * Math.PI,
  1341. series.xaxis, series.yaxis);
  1342. ctx.restore();
  1343. }
  1344. function drawBar(x, y, b, barLeft, barRight, offset, fillStyleCallback, axisx, axisy, c, horizontal) {
  1345. var left, right, bottom, top,
  1346. drawLeft, drawRight, drawTop, drawBottom,
  1347. tmp;
  1348. if (horizontal) {
  1349. drawBottom = drawRight = drawTop = true;
  1350. drawLeft = false;
  1351. left = b;
  1352. right = x;
  1353. top = y + barLeft;
  1354. bottom = y + barRight;
  1355. // account for negative bars
  1356. if (right < left) {
  1357. tmp = right;
  1358. right = left;
  1359. left = tmp;
  1360. drawLeft = true;
  1361. drawRight = false;
  1362. }
  1363. }
  1364. else {
  1365. drawLeft = drawRight = drawTop = true;
  1366. drawBottom = false;
  1367. left = x + barLeft;
  1368. right = x + barRight;
  1369. bottom = b;
  1370. top = y;
  1371. // account for negative bars
  1372. if (top < bottom) {
  1373. tmp = top;
  1374. top = bottom;
  1375. bottom = tmp;
  1376. drawBottom = true;
  1377. drawTop = false;
  1378. }
  1379. }
  1380. // clip
  1381. if (right < axisx.min || left > axisx.max ||
  1382. top < axisy.min || bottom > axisy.max)
  1383. return;
  1384. if (left < axisx.min) {
  1385. left = axisx.min;
  1386. drawLeft = false;
  1387. }
  1388. if (right > axisx.max) {
  1389. right = axisx.max;
  1390. drawRight = false;
  1391. }
  1392. if (bottom < axisy.min) {
  1393. bottom = axisy.min;
  1394. drawBottom = false;
  1395. }
  1396. if (top > axisy.max) {
  1397. top = axisy.max;
  1398. drawTop = false;
  1399. }
  1400. left = axisx.p2c(left);
  1401. bottom = axisy.p2c(bottom);
  1402. right = axisx.p2c(right);
  1403. top = axisy.p2c(top);
  1404. // fill the bar
  1405. if (fillStyleCallback) {
  1406. c.beginPath();
  1407. c.moveTo(left, bottom);
  1408. c.lineTo(left, top);
  1409. c.lineTo(right, top);
  1410. c.lineTo(right, bottom);
  1411. c.fillStyle = fillStyleCallback(bottom, top);
  1412. c.fill();
  1413. }
  1414. // draw outline
  1415. if (drawLeft || drawRight || drawTop || drawBottom) {
  1416. c.beginPath();
  1417. // FIXME: inline moveTo is buggy with excanvas
  1418. c.moveTo(left, bottom + offset);
  1419. if (drawLeft)
  1420. c.lineTo(left, top + offset);
  1421. else
  1422. c.moveTo(left, top + offset);
  1423. if (drawTop)
  1424. c.lineTo(right, top + offset);
  1425. else
  1426. c.moveTo(right, top + offset);
  1427. if (drawRight)
  1428. c.lineTo(right, bottom + offset);
  1429. else
  1430. c.moveTo(right, bottom + offset);
  1431. if (drawBottom)
  1432. c.lineTo(left, bottom + offset);
  1433. else
  1434. c.moveTo(left, bottom + offset);
  1435. c.stroke();
  1436. }
  1437. }
  1438. function drawSeriesBars(series) {
  1439. function plotBars(datapoints, barLeft, barRight, offset, fillStyleCallback, axisx, axisy) {
  1440. var points = datapoints.points, ps = datapoints.pointsize;
  1441. for (var i = 0; i < points.length; i += ps) {
  1442. if (points[i] == null)
  1443. continue;
  1444. drawBar(points[i], points[i + 1], points[i + 2], barLeft, barRight, offset, fillStyleCallback, axisx, axisy, ctx, series.bars.horizontal);
  1445. }
  1446. }
  1447. ctx.save();
  1448. ctx.translate(plotOffset.left, plotOffset.top);
  1449. // FIXME: figure out a way to add shadows (for instance along the right edge)
  1450. ctx.lineWidth = series.bars.lineWidth;
  1451. ctx.strokeStyle = series.color;
  1452. var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
  1453. var fillStyleCallback = series.bars.fill ? function (bottom, top) { return getFillStyle(series.bars, series.color, bottom, top); } : null;
  1454. plotBars(series.datapoints, barLeft, barLeft + series.bars.barWidth, 0, fillStyleCallback, series.xaxis, series.yaxis);
  1455. ctx.restore();
  1456. }
  1457. function getFillStyle(filloptions, seriesColor, bottom, top) {
  1458. var fill = filloptions.fill;
  1459. if (!fill)
  1460. return null;
  1461. if (filloptions.fillColor)
  1462. return getColorOrGradient(filloptions.fillColor, bottom, top, seriesColor);
  1463. var c = $.color.parse(seriesColor);
  1464. c.a = typeof fill == "number" ? fill : 0.4;
  1465. c.normalize();
  1466. return c.toString();
  1467. }
  1468. function insertLegend() {
  1469. placeholder.find(".legend").remove();
  1470. if (!options.legend.show)
  1471. return;
  1472. var fragments = [], rowStarted = false,
  1473. lf = options.legend.labelFormatter, s, label;
  1474. for (i = 0; i < series.length; ++i) {
  1475. s = series[i];
  1476. label = s.label;
  1477. if (!label)
  1478. continue;
  1479. if (i % options.legend.noColumns == 0) {
  1480. if (rowStarted)
  1481. fragments.push('</tr>');
  1482. fragments.push('<tr>');
  1483. rowStarted = true;
  1484. }
  1485. if (lf)
  1486. label = lf(label, s);
  1487. fragments.push(
  1488. '<td class="legendColorBox"><div style="border:1px solid ' + options.legend.labelBoxBorderColor + ';padding:1px"><div style="width:4px;height:0;border:5px solid ' + s.color + ';overflow:hidden"></div></div></td>' +
  1489. '<td class="legendLabel">' + label + '</td>');
  1490. }
  1491. if (rowStarted)
  1492. fragments.push('</tr>');
  1493. if (fragments.length == 0)
  1494. return;
  1495. var table = '<table style="font-size:smaller;color:' + options.grid.color + '">' + fragments.join("") + '</table>';
  1496. if (options.legend.container != null)
  1497. $(options.legend.container).html(table);
  1498. else {
  1499. var pos = "",
  1500. p = options.legend.position,
  1501. m = options.legend.margin;
  1502. if (m[0] == null)
  1503. m = [m, m];
  1504. if (p.charAt(0) == "n")
  1505. pos += 'top:' + (m[1] + plotOffset.top) + 'px;';
  1506. else if (p.charAt(0) == "s")
  1507. pos += 'bottom:' + (m[1] + plotOffset.bottom) + 'px;';
  1508. if (p.charAt(1) == "e")
  1509. pos += 'right:' + (m[0] + plotOffset.right) + 'px;';
  1510. else if (p.charAt(1) == "w")
  1511. pos += 'left:' + (m[0] + plotOffset.left) + 'px;';
  1512. var legend = $('<div class="legend">' + table.replace('style="', 'style="position:absolute;' + pos +';') + '</div>').appendTo(placeholder);
  1513. if (options.legend.backgroundOpacity != 0.0) {
  1514. // put in the transparent background
  1515. // separately to avoid blended labels and
  1516. // label boxes
  1517. var c = options.legend.backgroundColor;
  1518. if (c == null) {
  1519. c = options.grid.backgroundColor;
  1520. if (c && typeof c == "string")
  1521. c = $.color.parse(c);
  1522. else
  1523. c = $.color.extract(legend, 'background-color');
  1524. c.a = 1;
  1525. c = c.toString();
  1526. }
  1527. var div = legend.children();
  1528. $('<div style="position:absolute;width:' + div.width() + 'px;height:' + div.height() + 'px;' + pos +'background-color:' + c + ';"> </div>').prependTo(legend).css('opacity', options.legend.backgroundOpacity);
  1529. }
  1530. }
  1531. }
  1532. // interactive features
  1533. var highlights = [],
  1534. redrawTimeout = null;
  1535. // returns the data item the mouse is over, or null if none is found
  1536. function findNearbyItem(mouseX, mouseY, seriesFilter) {
  1537. var maxDistance = options.grid.mouseActiveRadius,
  1538. smallestDistance = maxDistance * maxDistance + 1,
  1539. item = null, foundPoint = false, i, j;
  1540. for (i = 0; i < series.length; ++i) {
  1541. if (!seriesFilter(series[i]))
  1542. continue;
  1543. var s = series[i],
  1544. axisx = s.xaxis,
  1545. axisy = s.yaxis,
  1546. points = s.datapoints.points,
  1547. ps = s.datapoints.pointsize,
  1548. mx = axisx.c2p(mouseX), // precompute some stuff to make the loop faster
  1549. my = axisy.c2p(mouseY),
  1550. maxx = maxDistance / axisx.scale,
  1551. maxy = maxDistance / axisy.scale;
  1552. if (s.lines.show || s.points.show) {
  1553. for (j = 0; j < points.length; j += ps) {
  1554. var x = points[j], y = points[j + 1];
  1555. if (x == null)
  1556. continue;
  1557. // For points and lines, the cursor must be within a
  1558. // certain distance to the data point
  1559. if (x - mx > maxx || x - mx < -maxx ||
  1560. y - my > maxy || y - my < -maxy)
  1561. continue;
  1562. // We have to calculate distances in pixels, not in
  1563. // data units, because the scales of the axes may be different
  1564. var dx = Math.abs(axisx.p2c(x) - mouseX),
  1565. dy = Math.abs(axisy.p2c(y) - mouseY),
  1566. dist = dx * dx + dy * dy; // we save the sqrt
  1567. // use <= to ensure last point takes precedence
  1568. // (last generally means on top of)
  1569. if (dist <= smallestDistance) {
  1570. smallestDistance = dist;
  1571. item = [i, j / ps];
  1572. }
  1573. }
  1574. }
  1575. if (s.bars.show && !item) { // no other point can be nearby
  1576. var barLeft = s.bars.align == "left" ? 0 : -s.bars.barWidth/2,
  1577. barRight = barLeft + s.bars.barWidth;
  1578. for (j = 0; j < points.length; j += ps) {
  1579. var x = points[j], y = points[j + 1], b = points[j + 2];
  1580. if (x == null)
  1581. continue;
  1582. // for a bar graph, the cursor must be inside the bar
  1583. if (series[i].bars.horizontal ?
  1584. (mx <= Math.max(b, x) && mx >= Math.min(b, x) &&
  1585. my >= y + barLeft && my <= y + barRight) :
  1586. (mx >= x + barLeft && mx <= x + barRight &&
  1587. my >= Math.min(b, y) && my <= Math.max(b, y)))
  1588. item = [i, j / ps];
  1589. }
  1590. }
  1591. }
  1592. if (item) {
  1593. i = item[0];
  1594. j = item[1];
  1595. ps = series[i].datapoints.pointsize;
  1596. return { datapoint: series[i].datapoints.points.slice(j * ps, (j + 1) * ps),
  1597. dataIndex: j,
  1598. series: series[i],
  1599. seriesIndex: i };
  1600. }
  1601. return null;
  1602. }
  1603. function onMouseMove(e) {
  1604. if (options.grid.hoverable)
  1605. triggerClickHoverEvent("plothover", e,
  1606. function (s) { return s["hoverable"] != false; });
  1607. }
  1608. function onClick(e) {
  1609. triggerClickHoverEvent("plotclick", e,
  1610. function (s) { return s["clickable"] != false; });
  1611. }
  1612. // trigger click or hover event (they send the same parameters
  1613. // so we share their code)
  1614. function triggerClickHoverEvent(eventname, event, seriesFilter) {
  1615. var offset = eventHolder.offset(),
  1616. pos = { pageX: event.pageX, pageY: event.pageY },
  1617. canvasX = event.pageX - offset.left - plotOffset.left,
  1618. canvasY = event.pageY - offset.top - plotOffset.top;
  1619. if (axes.xaxis.used)
  1620. pos.x = axes.xaxis.c2p(canvasX);
  1621. if (axes.yaxis.used)
  1622. pos.y = axes.yaxis.c2p(canvasY);
  1623. if (axes.x2axis.used)
  1624. pos.x2 = axes.x2axis.c2p(canvasX);
  1625. if (axes.y2axis.used)
  1626. pos.y2 = axes.y2axis.c2p(canvasY);
  1627. var item = findNearbyItem(canvasX, canvasY, seriesFilter);
  1628. if (item) {
  1629. // fill in mouse pos for any listeners out there
  1630. item.pageX = parseInt(item.series.xaxis.p2c(item.datapoint[0]) + offset.left + plotOffset.left);
  1631. item.pageY = parseInt(item.series.yaxis.p2c(item.datapoint[1]) + offset.top + plotOffset.top);
  1632. }
  1633. if (options.grid.autoHighlight) {
  1634. // clear auto-highlights
  1635. for (var i = 0; i < highlights.length; ++i) {
  1636. var h = highlights[i];
  1637. if (h.auto == eventname &&
  1638. !(item && h.series == item.series && h.point == item.datapoint))
  1639. unhighlight(h.series, h.point);
  1640. }
  1641. if (item)
  1642. highlight(item.series, item.datapoint, eventname);
  1643. }
  1644. placeholder.trigger(eventname, [ pos, item ]);
  1645. }
  1646. function triggerRedrawOverlay() {
  1647. if (!redrawTimeout)
  1648. redrawTimeout = setTimeout(drawOverlay, 30);
  1649. }
  1650. function drawOverlay() {
  1651. redrawTimeout = null;
  1652. // draw highlights
  1653. octx.save();
  1654. octx.clearRect(0, 0, canvasWidth, canvasHeight);
  1655. octx.translate(plotOffset.left, plotOffset.top);
  1656. var i, hi;
  1657. for (i = 0; i < highlights.length; ++i) {
  1658. hi = highlights[i];
  1659. if (hi.series.bars.show)
  1660. drawBarHighlight(hi.series, hi.point);
  1661. else
  1662. drawPointHighlight(hi.series, hi.point);
  1663. }
  1664. octx.restore();
  1665. executeHooks(hooks.drawOverlay, [octx]);
  1666. }
  1667. function highlight(s, point, auto) {
  1668. if (typeof s == "number")
  1669. s = series[s];
  1670. if (typeof point == "number")
  1671. point = s.data[point];
  1672. var i = indexOfHighlight(s, point);
  1673. if (i == -1) {
  1674. highlights.push({ series: s, point: point, auto: auto });
  1675. triggerRedrawOverlay();
  1676. }
  1677. else if (!auto)
  1678. highlights[i].auto = false;
  1679. }
  1680. function unhighlight(s, point) {
  1681. if (s == null && point == null) {
  1682. highlights = [];
  1683. triggerRedrawOverlay();
  1684. }
  1685. if (typeof s == "number")
  1686. s = series[s];
  1687. if (typeof point == "number")
  1688. point = s.data[point];
  1689. var i = indexOfHighlight(s, point);
  1690. if (i != -1) {
  1691. highlights.splice(i, 1);
  1692. triggerRedrawOverlay();
  1693. }
  1694. }
  1695. function indexOfHighlight(s, p) {
  1696. for (var i = 0; i < highlights.length; ++i) {
  1697. var h = highlights[i];
  1698. if (h.series == s && h.point[0] == p[0]
  1699. && h.point[1] == p[1])
  1700. return i;
  1701. }
  1702. return -1;
  1703. }
  1704. function drawPointHighlight(series, point) {
  1705. var x = point[0], y = point[1],
  1706. axisx = series.xaxis, axisy = series.yaxis;
  1707. if (x < axisx.min || x > axisx.max || y < axisy.min || y > axisy.max)
  1708. return;
  1709. var pointRadius = series.points.radius + series.points.lineWidth / 2;
  1710. octx.lineWidth = pointRadius;
  1711. octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  1712. var radius = 1.5 * pointRadius;
  1713. octx.beginPath();
  1714. octx.arc(axisx.p2c(x), axisy.p2c(y), radius, 0, 2 * Math.PI, false);
  1715. octx.stroke();
  1716. }
  1717. function drawBarHighlight(series, point) {
  1718. octx.lineWidth = series.bars.lineWidth;
  1719. octx.strokeStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  1720. var fillStyle = $.color.parse(series.color).scale('a', 0.5).toString();
  1721. var barLeft = series.bars.align == "left" ? 0 : -series.bars.barWidth/2;
  1722. drawBar(point[0], point[1], point[2] || 0, barLeft, barLeft + series.bars.barWidth,
  1723. 0, function () { return fillStyle; }, series.xaxis, series.yaxis, octx, series.bars.horizontal);
  1724. }
  1725. function getColorOrGradient(spec, bottom, top, defaultColor) {
  1726. if (typeof spec == "string")
  1727. return spec;
  1728. else {
  1729. // assume this is a gradient spec; IE currently only
  1730. // supports a simple vertical gradient properly, so that's
  1731. // what we support too
  1732. var gradient = ctx.createLinearGradient(0, top, 0, bottom);
  1733. for (var i = 0, l = spec.colors.length; i < l; ++i) {
  1734. var c = spec.colors[i];
  1735. if (typeof c != "string") {
  1736. c = $.color.parse(defaultColor).scale('rgb', c.brightness);
  1737. c.a *= c.opacity;
  1738. c = c.toString();
  1739. }
  1740. gradient.addColorStop(i / (l - 1), c);
  1741. }
  1742. return gradient;
  1743. }
  1744. }
  1745. }
  1746. $.plot = function(placeholder, data, options) {
  1747. var plot = new Plot($(placeholder), data, options, $.plot.plugins);
  1748. /*var t0 = new Date();
  1749. var t1 = new Date();
  1750. var tstr = "time used (msecs): " + (t1.getTime() - t0.getTime())
  1751. if (window.console)
  1752. console.log(tstr);
  1753. else
  1754. alert(tstr);*/
  1755. return plot;
  1756. };
  1757. $.plot.plugins = [];
  1758. // returns a string with the date d formatted according to fmt
  1759. $.plot.formatDate = function(d, fmt, monthNames) {
  1760. var leftPad = function(n) {
  1761. n = "" + n;
  1762. return n.length == 1 ? "0" + n : n;
  1763. };
  1764. var r = [];
  1765. var escape = false;
  1766. var hours = d.getUTCHours();
  1767. var isAM = hours < 12;
  1768. if (monthNames == null)
  1769. monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
  1770. if (fmt.search(/%p|%P/) != -1) {
  1771. if (hours > 12) {
  1772. hours = hours - 12;
  1773. } else if (hours == 0) {
  1774. hours = 12;
  1775. }
  1776. }
  1777. for (var i = 0; i < fmt.length; ++i) {
  1778. var c = fmt.charAt(i);
  1779. if (escape) {
  1780. switch (c) {
  1781. case 'h': c = "" + hours; break;
  1782. case 'H': c = leftPad(hours); break;
  1783. case 'M': c = leftPad(d.getUTCMinutes()); break;
  1784. case 'S': c = leftPad(d.getUTCSeconds()); break;
  1785. case 'd': c = "" + d.getUTCDate(); break;
  1786. case 'm': c = "" + (d.getUTCMonth() + 1); break;
  1787. case 'y': c = "" + d.getUTCFullYear(); break;
  1788. case 'b': c = "" + monthNames[d.getUTCMonth()]; break;
  1789. case 'p': c = (isAM) ? ("" + "am") : ("" + "pm"); break;
  1790. case 'P': c = (isAM) ? ("" + "AM") : ("" + "PM"); break;
  1791. }
  1792. r.push(c);
  1793. escape = false;
  1794. }
  1795. else {
  1796. if (c == "%")
  1797. escape = true;
  1798. else
  1799. r.push(c);
  1800. }
  1801. }
  1802. return r.join("");
  1803. };
  1804. // round to nearby lower multiple of base
  1805. function floorInBase(n, base) {
  1806. return base * Math.floor(n / base);
  1807. }
  1808. })(jQuery);