PHP/JavaScript webapp to analyse spending habits
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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);