smoothie.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // MIT License:
  2. //
  3. // Copyright (c) 2010-2011, Joe Walnes
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. /**
  23. * Smoothie Charts - http://smoothiecharts.org/
  24. * (c) 2010-2012, Joe Walnes
  25. *
  26. * v1.0: Main charting library, by Joe Walnes
  27. * v1.1: Auto scaling of axis, by Neil Dunn
  28. * v1.2: fps (frames per second) option, by Mathias Petterson
  29. * v1.3: Fix for divide by zero, by Paul Nikitochkin
  30. * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
  31. * v1.5: Set default frames per second to 50... smoother.
  32. * .start(), .stop() methods for conserving CPU, by Dmitry Vyal
  33. * options.iterpolation = 'bezier' or 'line', by Dmitry Vyal
  34. * options.maxValue to fix scale, by Dmitry Vyal
  35. * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
  36. * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
  37. * Smooth rescaling, by Kostas Michalopoulos
  38. * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
  39. * v1.9: Display timestamps along the bottom, by Nick and Stev-io
  40. * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
  41. * Refactored by Krishna Narni, to support timestamp formatting function
  42. */
  43. function TimeSeries(options) {
  44. options = options || {};
  45. options.resetBoundsInterval = options.resetBoundsInterval || 3000; // Reset the max/min bounds after this many milliseconds
  46. options.resetBounds = options.resetBounds === undefined ? true : options.resetBounds; // Enable or disable the resetBounds timer
  47. this.options = options;
  48. this.data = [];
  49. this.label = options.label || "";
  50. this.maxDataLength = options.maxDataLength || 1000;
  51. this.dataPool = [];
  52. this.maxValue = Number.NaN; // The maximum value ever seen in this time series.
  53. this.minValue = Number.NaN; // The minimum value ever seen in this time series.
  54. // Start a resetBounds Interval timer desired
  55. if (options.resetBounds) {
  56. this.boundsTimer = setInterval((function(thisObj) { return function() { thisObj.resetBounds(); } })(this), options.resetBoundsInterval);
  57. }
  58. }
  59. // Reset the min and max for this timeseries so the graph rescales itself
  60. TimeSeries.prototype.resetBounds = function() {
  61. this.maxValue = Number.NaN;
  62. this.minValue = Number.NaN;
  63. for (var i = 0; i < this.data.length; i++) {
  64. this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, this.data[i][1]) : this.data[i][1];
  65. this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, this.data[i][1]) : this.data[i][1];
  66. }
  67. };
  68. TimeSeries.prototype.append = function(timestamp, value) {
  69. this.lastTimeStamp = timestamp;
  70. var newData = this.dataPool.length ? this.dataPool.pop() : [timestamp, value];
  71. newData[0] = timestamp;
  72. newData[1] = value;
  73. this.data.push(newData);
  74. this.maxValue = !isNaN(this.maxValue) ? Math.max(this.maxValue, value) : value;
  75. this.minValue = !isNaN(this.minValue) ? Math.min(this.minValue, value) : value;
  76. while(this.data.length > this.maxDataLength)
  77. this.dataPool.push(this.data.shift());
  78. };
  79. function SmoothieChart(options) {
  80. // Defaults
  81. options = options || {};
  82. options.grid = options.grid || { fillStyle:'#000000', strokeStyle: '#777777', lineWidth: 1, millisPerLine: 1000, verticalSections: 2 };
  83. options.millisPerPixel = options.millisPerPixel || 20;
  84. options.fps = options.fps || 50;
  85. options.maxValueScale = options.maxValueScale || 1;
  86. options.minValue = options.minValue;
  87. options.maxValue = options.maxValue;
  88. options.labels = options.labels || { fillStyle:'#ffffff' };
  89. options.interpolation = options.interpolation || "bezier";
  90. options.scaleSmoothing = options.scaleSmoothing || 0.125;
  91. options.maxDataSetLength = options.maxDataSetLength || 2;
  92. options.timestampFormatter = options.timestampFormatter || null;
  93. this.options = options;
  94. this.seriesSet = [];
  95. this.currentValueRange = 1;
  96. this.currentVisMinValue = 0;
  97. }
  98. SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
  99. this.seriesSet.push({timeSeries: timeSeries, options: options || {}});
  100. };
  101. SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
  102. this.seriesSet.splice(this.seriesSet.indexOf(timeSeries), 1);
  103. };
  104. SmoothieChart.prototype.streamTo = function(canvas, delay) {
  105. var self = this;
  106. this.render_on_tick = function() {
  107. //self.render(canvas, new Date().getTime() - (delay || 0));
  108. var timeSeries = self.seriesSet[0].timeSeries;
  109. var dataSet = timeSeries.data;
  110. self.render(canvas, timeSeries.lastTimeStamp);
  111. };
  112. this.start();
  113. };
  114. SmoothieChart.prototype.start = function() {
  115. if (!this.timer)
  116. this.timer = setInterval(this.render_on_tick, 1000/this.options.fps);
  117. };
  118. SmoothieChart.prototype.stop = function() {
  119. if (this.timer) {
  120. clearInterval(this.timer);
  121. this.timer = undefined;
  122. }
  123. };
  124. // Sample timestamp formatting function
  125. SmoothieChart.timeFormatter = function(dateObject) {
  126. function pad2(number){return (number < 10 ? '0' : '') + number};
  127. return pad2(dateObject.getHours())+':'+pad2(dateObject.getMinutes())+':'+pad2(dateObject.getSeconds());
  128. };
  129. SmoothieChart.prototype.render = function(canvas, time) {
  130. var canvasContext = canvas.getContext("2d");
  131. var options = this.options;
  132. var dimensions = {top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight};
  133. // Save the state of the canvas context, any transformations applied in this method
  134. // will get removed from the stack at the end of this method when .restore() is called.
  135. canvasContext.save();
  136. // Round time down to pixel granularity, so motion appears smoother.
  137. time = time - time % options.millisPerPixel;
  138. // Move the origin.
  139. canvasContext.translate(dimensions.left, dimensions.top);
  140. // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
  141. // This prevents the occasional pixels from curves near the edges overrunning and creating
  142. // screen cheese (that phrase should neeed no explanation).
  143. canvasContext.beginPath();
  144. canvasContext.rect(0, 0, dimensions.width, dimensions.height);
  145. canvasContext.clip();
  146. // Clear the working area.
  147. canvasContext.save();
  148. canvasContext.fillStyle = options.grid.fillStyle;
  149. canvasContext.clearRect(0, 0, dimensions.width, dimensions.height);
  150. canvasContext.fillRect(0, 0, dimensions.width, dimensions.height);
  151. canvasContext.restore();
  152. // Grid lines....
  153. canvasContext.save();
  154. canvasContext.lineWidth = options.grid.lineWidth || 1;
  155. canvasContext.strokeStyle = options.grid.strokeStyle || '#ffffff';
  156. // Vertical (time) dividers.
  157. if (options.grid.millisPerLine > 0) {
  158. for (var t = time - (time % options.grid.millisPerLine); t >= time - (dimensions.width * options.millisPerPixel); t -= options.grid.millisPerLine) {
  159. canvasContext.beginPath();
  160. var gx = Math.round(dimensions.width - ((time - t) / options.millisPerPixel));
  161. canvasContext.moveTo(gx, 0);
  162. canvasContext.lineTo(gx, dimensions.height);
  163. canvasContext.stroke();
  164. // To display timestamps along the bottom
  165. // May have to adjust millisPerLine to display non-overlapping timestamps, depending on the canvas size
  166. if (options.timestampFormatter){
  167. var tx=new Date(t);
  168. // Formats the timestamp based on user specified formatting function
  169. // SmoothieChart.timeFormatter function above is one such formatting option
  170. var ts = options.timestampFormatter(tx);
  171. var txtwidth=(canvasContext.measureText(ts).width/2)+canvasContext.measureText(minValueString).width + 4;
  172. if (gx<dimensions.width - txtwidth){
  173. canvasContext.fillStyle = options.labels.fillStyle;
  174. // Insert the time string so it doesn't overlap on the minimum value
  175. canvasContext.fillText(ts, gx-(canvasContext.measureText(ts).width / 2), dimensions.height-2);
  176. }
  177. }
  178. canvasContext.closePath();
  179. }
  180. }
  181. // Horizontal (value) dividers.
  182. for (var v = 1; v < options.grid.verticalSections; v++) {
  183. var gy = Math.round(v * dimensions.height / options.grid.verticalSections);
  184. canvasContext.beginPath();
  185. canvasContext.moveTo(0, gy);
  186. canvasContext.lineTo(dimensions.width, gy);
  187. canvasContext.stroke();
  188. canvasContext.closePath();
  189. }
  190. // Bounding rectangle.
  191. canvasContext.beginPath();
  192. canvasContext.strokeRect(0, 0, dimensions.width, dimensions.height);
  193. canvasContext.closePath();
  194. canvasContext.restore();
  195. // Calculate the current scale of the chart, from all time series.
  196. var maxValue = Number.NaN;
  197. var minValue = Number.NaN;
  198. for (var d = 0; d < this.seriesSet.length; d++) {
  199. // TODO(ndunn): We could calculate / track these values as they stream in.
  200. var timeSeries = this.seriesSet[d].timeSeries;
  201. if (!isNaN(timeSeries.maxValue)) {
  202. maxValue = !isNaN(maxValue) ? Math.max(maxValue, timeSeries.maxValue) : timeSeries.maxValue;
  203. }
  204. if (!isNaN(timeSeries.minValue)) {
  205. minValue = !isNaN(minValue) ? Math.min(minValue, timeSeries.minValue) : timeSeries.minValue;
  206. }
  207. }
  208. if (isNaN(maxValue) && isNaN(minValue)) {
  209. canvasContext.restore(); // without this there is crash in Android browser
  210. return;
  211. }
  212. // Scale the maxValue to add padding at the top if required
  213. if (options.maxValue != null)
  214. maxValue = options.maxValue;
  215. else
  216. maxValue = maxValue * options.maxValueScale;
  217. // Set the minimum if we've specified one
  218. if (options.minValue != null)
  219. minValue = options.minValue;
  220. var targetValueRange = maxValue - minValue;
  221. this.currentValueRange += options.scaleSmoothing*(targetValueRange - this.currentValueRange);
  222. this.currentVisMinValue += options.scaleSmoothing*(minValue - this.currentVisMinValue);
  223. var valueRange = this.currentValueRange;
  224. var visMinValue = this.currentVisMinValue;
  225. // For each data set...
  226. for (var d = 0; d < this.seriesSet.length; d++) {
  227. canvasContext.save();
  228. var timeSeries = this.seriesSet[d].timeSeries;
  229. var dataSet = timeSeries.data;
  230. var seriesOptions = this.seriesSet[d].options;
  231. // Delete old data that's moved off the left of the chart.
  232. // We must always keep the last expired data point as we need this to draw the
  233. // line that comes into the chart, but any points prior to that can be removed.
  234. while (dataSet.length >= options.maxDataSetLength && dataSet[1][0] < time - (dimensions.width * options.millisPerPixel)) {
  235. dataSet.splice(0, 1);
  236. }
  237. // Set style for this dataSet.
  238. canvasContext.lineWidth = seriesOptions.lineWidth || 1;
  239. canvasContext.fillStyle = seriesOptions.fillStyle;
  240. canvasContext.strokeStyle = seriesOptions.strokeStyle || '#ffffff';
  241. // Draw the line...
  242. canvasContext.beginPath();
  243. // Retain lastX, lastY for calculating the control points of bezier curves.
  244. var firstX = 0, lastX = 0, lastY = 0;
  245. for (var i = 0; i < dataSet.length; i++) {
  246. // TODO: Deal with dataSet.length < 2.
  247. var x = Math.round(dimensions.width - ((time - dataSet[i][0]) / options.millisPerPixel));
  248. var value = dataSet[i][1];
  249. var offset = value - visMinValue;
  250. var scaledValue = dimensions.height - (valueRange ? Math.round((offset / valueRange) * dimensions.height) : 0);
  251. var y = Math.max(Math.min(scaledValue, dimensions.height - 1), 1); // Ensure line is always on chart.
  252. if (i == 0) {
  253. firstX = x;
  254. canvasContext.moveTo(x, y);
  255. }
  256. // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
  257. //
  258. // Assuming A was the last point in the line plotted and B is the new point,
  259. // we draw a curve with control points P and Q as below.
  260. //
  261. // A---P
  262. // |
  263. // |
  264. // |
  265. // Q---B
  266. //
  267. // Importantly, A and P are at the same y coordinate, as are B and Q. This is
  268. // so adjacent curves appear to flow as one.
  269. //
  270. else {
  271. switch (options.interpolation) {
  272. case "line":
  273. canvasContext.lineTo(x,y);
  274. break;
  275. case "bezier":
  276. default:
  277. canvasContext.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
  278. Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
  279. Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
  280. x, y); // endPoint (B)
  281. break;
  282. }
  283. }
  284. lastX = x, lastY = y;
  285. }
  286. if (dataSet.length > 0 && seriesOptions.fillStyle) {
  287. // Close up the fill region.
  288. canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
  289. canvasContext.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
  290. canvasContext.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
  291. canvasContext.fill();
  292. }
  293. canvasContext.stroke();
  294. canvasContext.closePath();
  295. canvasContext.restore();
  296. }
  297. // Draw the axis values on the chart.
  298. if (!options.labels.disabled) {
  299. if(!options.labelOffsetY)
  300. options.labelOffsetY = 0;
  301. canvasContext.fillStyle = options.labels.fillStyle;
  302. var maxValueString = parseFloat(maxValue).toFixed(2);
  303. var minValueString = parseFloat(minValue).toFixed(2);
  304. canvasContext.fillText(maxValueString, dimensions.width - canvasContext.measureText(maxValueString).width - 2, 10);
  305. canvasContext.fillText(minValueString, dimensions.width - canvasContext.measureText(minValueString).width - 2, dimensions.height - 2);
  306. for(var i=0; i<this.seriesSet.length; i++){
  307. var timeSeries = this.seriesSet[i].timeSeries;
  308. var label = timeSeries.label;
  309. canvasContext.fillStyle = timeSeries.options.fillStyle||"rgb(255,255,255)";
  310. if(label) canvasContext.fillText(label, 2, 10*(i+1) + options.labelOffsetY);
  311. }
  312. }
  313. canvasContext.restore(); // See .save() above.
  314. }