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

713 line
27KB

  1. // MIT License:
  2. //
  3. // Copyright (c) 2010-2013, 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-2013, Joe Walnes
  25. * 2013-2014, Drew Noakes
  26. *
  27. * v1.0: Main charting library, by Joe Walnes
  28. * v1.1: Auto scaling of axis, by Neil Dunn
  29. * v1.2: fps (frames per second) option, by Mathias Petterson
  30. * v1.3: Fix for divide by zero, by Paul Nikitochkin
  31. * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
  32. * v1.5: Set default frames per second to 50... smoother.
  33. * .start(), .stop() methods for conserving CPU, by Dmitry Vyal
  34. * options.interpolation = 'bezier' or 'line', by Dmitry Vyal
  35. * options.maxValue to fix scale, by Dmitry Vyal
  36. * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
  37. * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
  38. * Smooth rescaling, by Kostas Michalopoulos
  39. * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
  40. * v1.9: Display timestamps along the bottom, by Nick and Stev-io
  41. * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
  42. * Refactored by Krishna Narni, to support timestamp formatting function
  43. * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh
  44. * v1.11: options.grid.sharpLines option added, by @drewnoakes
  45. * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes
  46. * v1.12: Support for horizontalLines added, by @drewnoakes
  47. * Support for yRangeFunction callback added, by @drewnoakes
  48. * v1.13: Fixed typo (#32), by @alnikitich
  49. * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano
  50. * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes
  51. * v1.15: Support for npm package (#18), by @dominictarr
  52. * Fixed broken removeTimeSeries function (#24) by @davidgaleano
  53. * Minor performance and tidying, by @drewnoakes
  54. * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes
  55. * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12)
  56. * Documentation and some local variable renaming for clarity, by @drewnoakes
  57. * v1.17: Allow control over font size (#10), by @drewnoakes
  58. * Timestamp text won't overlap, by @drewnoakes
  59. * v1.18: Allow control of max/min label precision, by @drewnoakes
  60. * Added 'borderVisible' chart option, by @drewnoakes
  61. * Allow drawing series with fill but no stroke (line), by @drewnoakes
  62. * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai
  63. * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes
  64. * v1.21: Add 'step' interpolation mode, by @drewnoakes
  65. */
  66. ;(function(exports) {
  67. var Util = {
  68. extend: function() {
  69. arguments[0] = arguments[0] || {};
  70. for (var i = 1; i < arguments.length; i++)
  71. {
  72. for (var key in arguments[i])
  73. {
  74. if (arguments[i].hasOwnProperty(key))
  75. {
  76. if (typeof(arguments[i][key]) === 'object') {
  77. if (arguments[i][key] instanceof Array) {
  78. arguments[0][key] = arguments[i][key];
  79. } else {
  80. arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]);
  81. }
  82. } else {
  83. arguments[0][key] = arguments[i][key];
  84. }
  85. }
  86. }
  87. }
  88. return arguments[0];
  89. }
  90. };
  91. /**
  92. * Initialises a new <code>TimeSeries</code> with optional data options.
  93. *
  94. * Options are of the form (defaults shown):
  95. *
  96. * <pre>
  97. * {
  98. * resetBounds: true, // enables/disables automatic scaling of the y-axis
  99. * resetBoundsInterval: 3000 // the period between scaling calculations, in millis
  100. * }
  101. * </pre>
  102. *
  103. * Presentation options for TimeSeries are specified as an argument to <code>SmoothieChart.addTimeSeries</code>.
  104. *
  105. * @constructor
  106. */
  107. function TimeSeries(options) {
  108. this.options = Util.extend({}, TimeSeries.defaultOptions, options);
  109. this.data = [];
  110. this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
  111. this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
  112. }
  113. TimeSeries.defaultOptions = {
  114. resetBoundsInterval: 3000,
  115. resetBounds: true
  116. };
  117. /**
  118. * Recalculate the min/max values for this <code>TimeSeries</code> object.
  119. *
  120. * This causes the graph to scale itself in the y-axis.
  121. */
  122. TimeSeries.prototype.resetBounds = function() {
  123. if (this.data.length) {
  124. // Walk through all data points, finding the min/max value
  125. this.maxValue = this.data[0][1];
  126. this.minValue = this.data[0][1];
  127. for (var i = 1; i < this.data.length; i++) {
  128. var value = this.data[i][1];
  129. if (value > this.maxValue) {
  130. this.maxValue = value;
  131. }
  132. if (value < this.minValue) {
  133. this.minValue = value;
  134. }
  135. }
  136. } else {
  137. // No data exists, so set min/max to NaN
  138. this.maxValue = Number.NaN;
  139. this.minValue = Number.NaN;
  140. }
  141. };
  142. /**
  143. * Adds a new data point to the <code>TimeSeries</code>, preserving chronological order.
  144. *
  145. * @param timestamp the position, in time, of this data point
  146. * @param value the value of this data point
  147. * @param sumRepeatedTimeStampValues if <code>timestamp</code> has an exact match in the series, this flag controls
  148. * whether it is replaced, or the values summed (defaults to false.)
  149. */
  150. TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
  151. // Rewind until we hit an older timestamp
  152. var i = this.data.length - 1;
  153. while (i > 0 && this.data[i][0] > timestamp) {
  154. i--;
  155. }
  156. if (this.data.length > 0 && this.data[i][0] === timestamp) {
  157. // Update existing values in the array
  158. if (sumRepeatedTimeStampValues) {
  159. // Sum this value into the existing 'bucket'
  160. this.data[i][1] += value;
  161. value = this.data[i][1];
  162. } else {
  163. // Replace the previous value
  164. this.data[i][1] = value;
  165. }
  166. } else if (i < this.data.length - 1) {
  167. // Splice into the correct position to keep timestamps in order
  168. this.data.splice(i + 1, 0, [timestamp, value]);
  169. } else {
  170. // Add to the end of the array
  171. this.data.push([timestamp, value]);
  172. }
  173. this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
  174. this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
  175. };
  176. TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
  177. // We must always keep one expired data point as we need this to draw the
  178. // line that comes into the chart from the left, but any points prior to that can be removed.
  179. var removeCount = 0;
  180. while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
  181. removeCount++;
  182. }
  183. if (removeCount !== 0) {
  184. this.data.splice(0, removeCount);
  185. }
  186. };
  187. /**
  188. * Initialises a new <code>SmoothieChart</code>.
  189. *
  190. * Options are optional, and should be of the form below. Just specify the values you
  191. * need and the rest will be given sensible defaults as shown:
  192. *
  193. * <pre>
  194. * {
  195. * minValue: undefined, // specify to clamp the lower y-axis to a given value
  196. * maxValue: undefined, // specify to clamp the upper y-axis to a given value
  197. * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
  198. * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; }
  199. * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs
  200. * millisPerPixel: 20, // sets the speed at which the chart pans by
  201. * maxDataSetLength: 2,
  202. * interpolation: 'bezier' // or 'linear'
  203. * timestampFormatter: null, // Optional function to format time stamps for bottom of chart. You may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
  204. * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ],
  205. * grid:
  206. * {
  207. * fillStyle: '#000000', // the background colour of the chart
  208. * lineWidth: 1, // the pixel width of grid lines
  209. * strokeStyle: '#777777', // colour of grid lines
  210. * millisPerLine: 1000, // distance between vertical grid lines
  211. * sharpLines: false, // controls whether grid lines are 1px sharp, or softened
  212. * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines
  213. * borderVisible: true // whether the grid lines trace the border of the chart or not
  214. * },
  215. * labels
  216. * {
  217. * disabled: false, // enables/disables labels showing the min/max values
  218. * fillStyle: '#ffffff', // colour for text of labels,
  219. * fontSize: 15,
  220. * fontFamily: 'sans-serif',
  221. * precision: 2
  222. * },
  223. * }
  224. * </pre>
  225. *
  226. * @constructor
  227. */
  228. function SmoothieChart(options) {
  229. this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options);
  230. this.seriesSet = [];
  231. this.currentValueRange = 1;
  232. this.currentVisMinValue = 0;
  233. this.lastRenderTimeMillis = 0;
  234. }
  235. SmoothieChart.defaultChartOptions = {
  236. millisPerPixel: 20,
  237. maxValueScale: 1,
  238. interpolation: 'bezier',
  239. scaleSmoothing: 0.125,
  240. maxDataSetLength: 2,
  241. grid: {
  242. fillStyle: '#000000',
  243. strokeStyle: '#777777',
  244. lineWidth: 1,
  245. sharpLines: false,
  246. millisPerLine: 1000,
  247. verticalSections: 2,
  248. borderVisible: true
  249. },
  250. labels: {
  251. fillStyle: '#ffffff',
  252. disabled: false,
  253. fontSize: 10,
  254. fontFamily: 'monospace',
  255. precision: 2
  256. },
  257. horizontalLines: []
  258. };
  259. // Based on http://inspirit.github.com/jsfeat/js/compatibility.js
  260. SmoothieChart.AnimateCompatibility = (function() {
  261. var requestAnimationFrame = function(callback, element) {
  262. var requestAnimationFrame =
  263. window.requestAnimationFrame ||
  264. window.webkitRequestAnimationFrame ||
  265. window.mozRequestAnimationFrame ||
  266. window.oRequestAnimationFrame ||
  267. window.msRequestAnimationFrame ||
  268. function(callback) {
  269. return window.setTimeout(function() {
  270. callback(new Date().getTime());
  271. }, 16);
  272. };
  273. return requestAnimationFrame.call(window, callback, element);
  274. },
  275. cancelAnimationFrame = function(id) {
  276. var cancelAnimationFrame =
  277. window.cancelAnimationFrame ||
  278. function(id) {
  279. clearTimeout(id);
  280. };
  281. return cancelAnimationFrame.call(window, id);
  282. };
  283. return {
  284. requestAnimationFrame: requestAnimationFrame,
  285. cancelAnimationFrame: cancelAnimationFrame
  286. };
  287. })();
  288. SmoothieChart.defaultSeriesPresentationOptions = {
  289. lineWidth: 1,
  290. strokeStyle: '#ffffff'
  291. };
  292. /**
  293. * Adds a <code>TimeSeries</code> to this chart, with optional presentation options.
  294. *
  295. * Presentation options should be of the form (defaults shown):
  296. *
  297. * <pre>
  298. * {
  299. * lineWidth: 1,
  300. * strokeStyle: '#ffffff',
  301. * fillStyle: undefined
  302. * }
  303. * </pre>
  304. */
  305. SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
  306. this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
  307. if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
  308. timeSeries.resetBoundsTimerId = setInterval(
  309. function() {
  310. timeSeries.resetBounds();
  311. },
  312. timeSeries.options.resetBoundsInterval
  313. );
  314. }
  315. };
  316. /**
  317. * Removes the specified <code>TimeSeries</code> from the chart.
  318. */
  319. SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
  320. // Find the correct timeseries to remove, and remove it
  321. var numSeries = this.seriesSet.length;
  322. for (var i = 0; i < numSeries; i++) {
  323. if (this.seriesSet[i].timeSeries === timeSeries) {
  324. this.seriesSet.splice(i, 1);
  325. break;
  326. }
  327. }
  328. // If a timer was operating for that timeseries, remove it
  329. if (timeSeries.resetBoundsTimerId) {
  330. // Stop resetting the bounds, if we were
  331. clearInterval(timeSeries.resetBoundsTimerId);
  332. }
  333. };
  334. /**
  335. * Gets render options for the specified <code>TimeSeries</code>.
  336. *
  337. * As you may use a single <code>TimeSeries</code> in multiple charts with different formatting in each usage,
  338. * these settings are stored in the chart.
  339. */
  340. SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) {
  341. // Find the correct timeseries to remove, and remove it
  342. var numSeries = this.seriesSet.length;
  343. for (var i = 0; i < numSeries; i++) {
  344. if (this.seriesSet[i].timeSeries === timeSeries) {
  345. return this.seriesSet[i].options;
  346. }
  347. }
  348. };
  349. /**
  350. * Brings the specified <code>TimeSeries</code> to the top of the chart. It will be rendered last.
  351. */
  352. SmoothieChart.prototype.bringToFront = function(timeSeries) {
  353. // Find the correct timeseries to remove, and remove it
  354. var numSeries = this.seriesSet.length;
  355. for (var i = 0; i < numSeries; i++) {
  356. if (this.seriesSet[i].timeSeries === timeSeries) {
  357. var set = this.seriesSet.splice(i, 1);
  358. this.seriesSet.push(set[0]);
  359. break;
  360. }
  361. }
  362. };
  363. /**
  364. * Instructs the <code>SmoothieChart</code> to start rendering to the provided canvas, with specified delay.
  365. *
  366. * @param canvas the target canvas element
  367. * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
  368. * from appearing on screen, with new values flashing into view, at the expense of some latency.
  369. */
  370. SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
  371. this.canvas = canvas;
  372. this.delay = delayMillis;
  373. this.start();
  374. };
  375. /**
  376. * Starts the animation of this chart.
  377. */
  378. SmoothieChart.prototype.start = function() {
  379. if (this.frame) {
  380. // We're already running, so just return
  381. return;
  382. }
  383. // Renders a frame, and queues the next frame for later rendering
  384. var animate = function() {
  385. this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
  386. this.render();
  387. animate();
  388. }.bind(this));
  389. }.bind(this);
  390. animate();
  391. };
  392. /**
  393. * Stops the animation of this chart.
  394. */
  395. SmoothieChart.prototype.stop = function() {
  396. if (this.frame) {
  397. SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
  398. delete this.frame;
  399. }
  400. };
  401. SmoothieChart.prototype.updateValueRange = function() {
  402. // Calculate the current scale of the chart, from all time series.
  403. var chartOptions = this.options,
  404. chartMaxValue = Number.NaN,
  405. chartMinValue = Number.NaN;
  406. for (var d = 0; d < this.seriesSet.length; d++) {
  407. // TODO(ndunn): We could calculate / track these values as they stream in.
  408. var timeSeries = this.seriesSet[d].timeSeries;
  409. if (!isNaN(timeSeries.maxValue)) {
  410. chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
  411. }
  412. if (!isNaN(timeSeries.minValue)) {
  413. chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
  414. }
  415. }
  416. // Scale the chartMaxValue to add padding at the top if required
  417. if (chartOptions.maxValue != null) {
  418. chartMaxValue = chartOptions.maxValue;
  419. } else {
  420. chartMaxValue *= chartOptions.maxValueScale;
  421. }
  422. // Set the minimum if we've specified one
  423. if (chartOptions.minValue != null) {
  424. chartMinValue = chartOptions.minValue;
  425. }
  426. // If a custom range function is set, call it
  427. if (this.options.yRangeFunction) {
  428. var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
  429. chartMinValue = range.min;
  430. chartMaxValue = range.max;
  431. }
  432. if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
  433. var targetValueRange = chartMaxValue - chartMinValue;
  434. var valueRangeDiff = (targetValueRange - this.currentValueRange);
  435. var minValueDiff = (chartMinValue - this.currentVisMinValue);
  436. this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
  437. this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
  438. this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
  439. }
  440. this.valueRange = { min: chartMinValue, max: chartMaxValue };
  441. };
  442. SmoothieChart.prototype.render = function(canvas, time) {
  443. var nowMillis = new Date().getTime();
  444. if (!this.isAnimatingScale) {
  445. // We're not animating. We can use the last render time and the scroll speed to work out whether
  446. // we actually need to paint anything yet. If not, we can return immediately.
  447. // Render at least every 1/6th of a second. The canvas may be resized, which there is
  448. // no reliable way to detect.
  449. var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
  450. if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
  451. return;
  452. }
  453. }
  454. this.lastRenderTimeMillis = nowMillis;
  455. canvas = canvas || this.canvas;
  456. time = time || nowMillis - (this.delay || 0);
  457. // Round time down to pixel granularity, so motion appears smoother.
  458. time -= time % this.options.millisPerPixel;
  459. var context = canvas.getContext('2d'),
  460. chartOptions = this.options,
  461. dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
  462. // Calculate the threshold time for the oldest data points.
  463. oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
  464. valueToYPixel = function(value) {
  465. var offset = value - this.currentVisMinValue;
  466. return this.currentValueRange === 0
  467. ? dimensions.height
  468. : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
  469. }.bind(this),
  470. timeToXPixel = function(t) {
  471. return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
  472. };
  473. this.updateValueRange();
  474. context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
  475. // Save the state of the canvas context, any transformations applied in this method
  476. // will get removed from the stack at the end of this method when .restore() is called.
  477. context.save();
  478. // Move the origin.
  479. context.translate(dimensions.left, dimensions.top);
  480. // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
  481. // This prevents the occasional pixels from curves near the edges overrunning and creating
  482. // screen cheese (that phrase should need no explanation).
  483. context.beginPath();
  484. context.rect(0, 0, dimensions.width, dimensions.height);
  485. context.clip();
  486. // Clear the working area.
  487. context.save();
  488. context.fillStyle = chartOptions.grid.fillStyle;
  489. context.clearRect(0, 0, dimensions.width, dimensions.height);
  490. context.fillRect(0, 0, dimensions.width, dimensions.height);
  491. context.restore();
  492. // Grid lines...
  493. context.save();
  494. context.lineWidth = chartOptions.grid.lineWidth;
  495. context.strokeStyle = chartOptions.grid.strokeStyle;
  496. // Vertical (time) dividers.
  497. if (chartOptions.grid.millisPerLine > 0) {
  498. var textUntilX = dimensions.width - context.measureText(minValueString).width + 4;
  499. for (var t = time - (time % chartOptions.grid.millisPerLine);
  500. t >= oldestValidTime;
  501. t -= chartOptions.grid.millisPerLine) {
  502. var gx = timeToXPixel(t);
  503. if (chartOptions.grid.sharpLines) {
  504. gx -= 0.5;
  505. }
  506. context.beginPath();
  507. context.moveTo(gx, 0);
  508. context.lineTo(gx, dimensions.height);
  509. context.stroke();
  510. context.closePath();
  511. // Display timestamp at bottom of this line if requested, and it won't overlap
  512. if (chartOptions.timestampFormatter && gx < textUntilX) {
  513. // Formats the timestamp based on user specified formatting function
  514. // SmoothieChart.timeFormatter function above is one such formatting option
  515. var tx = new Date(t),
  516. ts = chartOptions.timestampFormatter(tx),
  517. tsWidth = context.measureText(ts).width;
  518. textUntilX = gx - tsWidth - 2;
  519. context.fillStyle = chartOptions.labels.fillStyle;
  520. context.fillText(ts, gx - tsWidth, dimensions.height - 2);
  521. }
  522. }
  523. }
  524. // Horizontal (value) dividers.
  525. for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
  526. var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
  527. if (chartOptions.grid.sharpLines) {
  528. gy -= 0.5;
  529. }
  530. context.beginPath();
  531. context.moveTo(0, gy);
  532. context.lineTo(dimensions.width, gy);
  533. context.stroke();
  534. context.closePath();
  535. }
  536. // Bounding rectangle.
  537. if (chartOptions.grid.borderVisible) {
  538. context.beginPath();
  539. context.strokeRect(0, 0, dimensions.width, dimensions.height);
  540. context.closePath();
  541. }
  542. context.restore();
  543. // Draw any horizontal lines...
  544. if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
  545. for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
  546. var line = chartOptions.horizontalLines[hl],
  547. hly = Math.round(valueToYPixel(line.value)) - 0.5;
  548. context.strokeStyle = line.color || '#ffffff';
  549. context.lineWidth = line.lineWidth || 1;
  550. context.beginPath();
  551. context.moveTo(0, hly);
  552. context.lineTo(dimensions.width, hly);
  553. context.stroke();
  554. context.closePath();
  555. }
  556. }
  557. // For each data set...
  558. for (var d = 0; d < this.seriesSet.length; d++) {
  559. context.save();
  560. var timeSeries = this.seriesSet[d].timeSeries,
  561. dataSet = timeSeries.data,
  562. seriesOptions = this.seriesSet[d].options;
  563. // Delete old data that's moved off the left of the chart.
  564. timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
  565. // Set style for this dataSet.
  566. context.lineWidth = seriesOptions.lineWidth;
  567. context.strokeStyle = seriesOptions.strokeStyle;
  568. // Draw the line...
  569. context.beginPath();
  570. // Retain lastX, lastY for calculating the control points of bezier curves.
  571. var firstX = 0, lastX = 0, lastY = 0;
  572. for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
  573. var x = timeToXPixel(dataSet[i][0]),
  574. y = valueToYPixel(dataSet[i][1]);
  575. if (i === 0) {
  576. firstX = x;
  577. context.moveTo(x, y);
  578. } else {
  579. switch (chartOptions.interpolation) {
  580. case "linear":
  581. case "line": {
  582. context.lineTo(x,y);
  583. break;
  584. }
  585. case "bezier":
  586. default: {
  587. // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
  588. //
  589. // Assuming A was the last point in the line plotted and B is the new point,
  590. // we draw a curve with control points P and Q as below.
  591. //
  592. // A---P
  593. // |
  594. // |
  595. // |
  596. // Q---B
  597. //
  598. // Importantly, A and P are at the same y coordinate, as are B and Q. This is
  599. // so adjacent curves appear to flow as one.
  600. //
  601. context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
  602. Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
  603. Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
  604. x, y); // endPoint (B)
  605. break;
  606. }
  607. case "step": {
  608. context.lineTo(x,lastY);
  609. context.lineTo(x,y);
  610. break;
  611. }
  612. }
  613. }
  614. lastX = x; lastY = y;
  615. }
  616. if (dataSet.length > 1) {
  617. if (seriesOptions.fillStyle) {
  618. // Close up the fill region.
  619. context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
  620. context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
  621. context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
  622. context.fillStyle = seriesOptions.fillStyle;
  623. context.fill();
  624. }
  625. if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
  626. context.stroke();
  627. }
  628. context.closePath();
  629. }
  630. context.restore();
  631. }
  632. // Draw the axis values on the chart.
  633. if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
  634. var maxValueString = parseFloat(this.valueRange.max).toFixed(chartOptions.labels.precision),
  635. minValueString = parseFloat(this.valueRange.min).toFixed(chartOptions.labels.precision);
  636. context.fillStyle = chartOptions.labels.fillStyle;
  637. context.fillText(maxValueString, dimensions.width - context.measureText(maxValueString).width - 2, chartOptions.labels.fontSize);
  638. context.fillText(minValueString, dimensions.width - context.measureText(minValueString).width - 2, dimensions.height - 2);
  639. }
  640. context.restore(); // See .save() above.
  641. };
  642. // Sample timestamp formatting function
  643. SmoothieChart.timeFormatter = function(date) {
  644. function pad2(number) { return (number < 10 ? '0' : '') + number }
  645. return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
  646. };
  647. exports.TimeSeries = TimeSeries;
  648. exports.SmoothieChart = SmoothieChart;
  649. })(typeof exports === 'undefined' ? this : exports);