smoothie.js 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104
  1. // MIT License:
  2. //
  3. // Copyright (c) 2010-2013, Joe Walnes
  4. // 2013-2018, Drew Noakes
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. /**
  24. * Smoothie Charts - http://smoothiecharts.org/
  25. * (c) 2010-2013, Joe Walnes
  26. * 2013-2018, Drew Noakes
  27. *
  28. * v1.0: Main charting library, by Joe Walnes
  29. * v1.1: Auto scaling of axis, by Neil Dunn
  30. * v1.2: fps (frames per second) option, by Mathias Petterson
  31. * v1.3: Fix for divide by zero, by Paul Nikitochkin
  32. * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds
  33. * v1.5: Set default frames per second to 50... smoother.
  34. * .start(), .stop() methods for conserving CPU, by Dmitry Vyal
  35. * options.interpolation = 'bezier' or 'line', by Dmitry Vyal
  36. * options.maxValue to fix scale, by Dmitry Vyal
  37. * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla
  38. * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin
  39. * Smooth rescaling, by Kostas Michalopoulos
  40. * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni
  41. * v1.9: Display timestamps along the bottom, by Nick and Stev-io
  42. * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D)
  43. * Refactored by Krishna Narni, to support timestamp formatting function
  44. * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh
  45. * v1.11: options.grid.sharpLines option added, by @drewnoakes
  46. * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes
  47. * v1.12: Support for horizontalLines added, by @drewnoakes
  48. * Support for yRangeFunction callback added, by @drewnoakes
  49. * v1.13: Fixed typo (#32), by @alnikitich
  50. * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano
  51. * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes
  52. * v1.15: Support for npm package (#18), by @dominictarr
  53. * Fixed broken removeTimeSeries function (#24) by @davidgaleano
  54. * Minor performance and tidying, by @drewnoakes
  55. * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes
  56. * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12)
  57. * Documentation and some local variable renaming for clarity, by @drewnoakes
  58. * v1.17: Allow control over font size (#10), by @drewnoakes
  59. * Timestamp text won't overlap, by @drewnoakes
  60. * v1.18: Allow control of max/min label precision, by @drewnoakes
  61. * Added 'borderVisible' chart option, by @drewnoakes
  62. * Allow drawing series with fill but no stroke (line), by @drewnoakes
  63. * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai
  64. * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes
  65. * v1.21: Add 'step' interpolation mode, by @drewnoakes
  66. * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic
  67. * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes
  68. * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf
  69. * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92
  70. * Draw time labels on top of series, by @comolosabia
  71. * Add TimeSeries.clear function, by @drewnoakes
  72. * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic
  73. * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush
  74. * v1.28: Add 'minValueScale' option, by @megawac
  75. * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn
  76. * v1.29: Support responsive sizing, by @drewnoakes
  77. * v1.29.1: Include types in package, and make property optional, by @TrentHouliston
  78. * v1.30: Fix inverted logic in devicePixelRatio support, by @scanlime
  79. * v1.31: Support tooltips, by @Sly1024 and @drewnoakes
  80. * v1.32: Support frame rate limit, by @dpuyosa
  81. * v1.33: Use Date static method instead of instance, by @nnnoel
  82. * Fix bug with tooltips when multiple charts on a page, by @jpmbiz70
  83. * v1.34: Add disabled option to TimeSeries, by @TechGuard (#91)
  84. * Add nonRealtimeData option, by @annazhelt (#92, #93)
  85. * Add showIntermediateLabels option, by @annazhelt (#94)
  86. * Add displayDataFromPercentile option, by @annazhelt (#95)
  87. * Fix bug when hiding tooltip element, by @ralphwetzel (#96)
  88. * Support intermediate y-axis labels, by @beikeland (#99)
  89. * v1.35: Fix issue with responsive mode at high DPI, by @drewnoakes (#101)
  90. * v1.36: Add tooltipLabel to ITimeSeriesPresentationOptions.
  91. * If tooltipLabel is present, tooltipLabel displays inside tooltip
  92. * next to value, by @jackdesert (#102)
  93. * Fix bug rendering issue in series fill when using scroll backwards, by @olssonfredrik
  94. * Add title option, by @mesca
  95. */
  96. ;(function(exports) {
  97. // Date.now polyfill
  98. Date.now = Date.now || function() { return new Date().getTime(); };
  99. var Util = {
  100. extend: function() {
  101. arguments[0] = arguments[0] || {};
  102. for (var i = 1; i < arguments.length; i++)
  103. {
  104. for (var key in arguments[i])
  105. {
  106. if (arguments[i].hasOwnProperty(key))
  107. {
  108. if (typeof(arguments[i][key]) === 'object') {
  109. if (arguments[i][key] instanceof Array) {
  110. arguments[0][key] = arguments[i][key];
  111. } else {
  112. arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]);
  113. }
  114. } else {
  115. arguments[0][key] = arguments[i][key];
  116. }
  117. }
  118. }
  119. }
  120. return arguments[0];
  121. },
  122. binarySearch: function(data, value) {
  123. var low = 0,
  124. high = data.length;
  125. while (low < high) {
  126. var mid = (low + high) >> 1;
  127. if (value < data[mid][0])
  128. high = mid;
  129. else
  130. low = mid + 1;
  131. }
  132. return low;
  133. }
  134. };
  135. /**
  136. * Initialises a new <code>TimeSeries</code> with optional data options.
  137. *
  138. * Options are of the form (defaults shown):
  139. *
  140. * <pre>
  141. * {
  142. * resetBounds: true, // enables/disables automatic scaling of the y-axis
  143. * resetBoundsInterval: 3000 // the period between scaling calculations, in millis
  144. * }
  145. * </pre>
  146. *
  147. * Presentation options for TimeSeries are specified as an argument to <code>SmoothieChart.addTimeSeries</code>.
  148. *
  149. * @constructor
  150. */
  151. function TimeSeries(options) {
  152. this.options = Util.extend({}, TimeSeries.defaultOptions, options);
  153. this.disabled = false;
  154. this.clear();
  155. }
  156. TimeSeries.defaultOptions = {
  157. resetBoundsInterval: 3000,
  158. resetBounds: true
  159. };
  160. /**
  161. * Clears all data and state from this TimeSeries object.
  162. */
  163. TimeSeries.prototype.clear = function() {
  164. this.data = [];
  165. this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries.
  166. this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries.
  167. };
  168. /**
  169. * Recalculate the min/max values for this <code>TimeSeries</code> object.
  170. *
  171. * This causes the graph to scale itself in the y-axis.
  172. */
  173. TimeSeries.prototype.resetBounds = function() {
  174. if (this.data.length) {
  175. // Walk through all data points, finding the min/max value
  176. this.maxValue = this.data[0][1];
  177. this.minValue = this.data[0][1];
  178. for (var i = 1; i < this.data.length; i++) {
  179. var value = this.data[i][1];
  180. if (value > this.maxValue) {
  181. this.maxValue = value;
  182. }
  183. if (value < this.minValue) {
  184. this.minValue = value;
  185. }
  186. }
  187. } else {
  188. // No data exists, so set min/max to NaN
  189. this.maxValue = Number.NaN;
  190. this.minValue = Number.NaN;
  191. }
  192. };
  193. /**
  194. * Adds a new data point to the <code>TimeSeries</code>, preserving chronological order.
  195. *
  196. * @param timestamp the position, in time, of this data point
  197. * @param value the value of this data point
  198. * @param sumRepeatedTimeStampValues if <code>timestamp</code> has an exact match in the series, this flag controls
  199. * whether it is replaced, or the values summed (defaults to false.)
  200. */
  201. TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) {
  202. // Rewind until we hit an older timestamp
  203. var i = this.data.length - 1;
  204. while (i >= 0 && this.data[i][0] > timestamp) {
  205. i--;
  206. }
  207. if (i === -1) {
  208. // This new item is the oldest data
  209. this.data.splice(0, 0, [timestamp, value]);
  210. } else if (this.data.length > 0 && this.data[i][0] === timestamp) {
  211. // Update existing values in the array
  212. if (sumRepeatedTimeStampValues) {
  213. // Sum this value into the existing 'bucket'
  214. this.data[i][1] += value;
  215. value = this.data[i][1];
  216. } else {
  217. // Replace the previous value
  218. this.data[i][1] = value;
  219. }
  220. } else if (i < this.data.length - 1) {
  221. // Splice into the correct position to keep timestamps in order
  222. this.data.splice(i + 1, 0, [timestamp, value]);
  223. } else {
  224. // Add to the end of the array
  225. this.data.push([timestamp, value]);
  226. }
  227. this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value);
  228. this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value);
  229. };
  230. TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) {
  231. // We must always keep one expired data point as we need this to draw the
  232. // line that comes into the chart from the left, but any points prior to that can be removed.
  233. var removeCount = 0;
  234. while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) {
  235. removeCount++;
  236. }
  237. if (removeCount !== 0) {
  238. this.data.splice(0, removeCount);
  239. }
  240. };
  241. /**
  242. * Initialises a new <code>SmoothieChart</code>.
  243. *
  244. * Options are optional, and should be of the form below. Just specify the values you
  245. * need and the rest will be given sensible defaults as shown:
  246. *
  247. * <pre>
  248. * {
  249. * minValue: undefined, // specify to clamp the lower y-axis to a given value
  250. * maxValue: undefined, // specify to clamp the upper y-axis to a given value
  251. * maxValueScale: 1, // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
  252. * minValueScale: 1, // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
  253. * yRangeFunction: undefined, // function({min: , max: }) { return {min: , max: }; }
  254. * scaleSmoothing: 0.125, // controls the rate at which y-value zoom animation occurs
  255. * millisPerPixel: 20, // sets the speed at which the chart pans by
  256. * enableDpiScaling: true, // support rendering at different DPI depending on the device
  257. * yMinFormatter: function(min, precision) { // callback function that formats the min y value label
  258. * return parseFloat(min).toFixed(precision);
  259. * },
  260. * yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
  261. * return parseFloat(max).toFixed(precision);
  262. * },
  263. * yIntermediateFormatter: function(intermediate, precision) { // callback function that formats the intermediate y value labels
  264. * return parseFloat(intermediate).toFixed(precision);
  265. * },
  266. * maxDataSetLength: 2,
  267. * interpolation: 'bezier' // one of 'bezier', 'linear', or 'step'
  268. * timestampFormatter: null, // optional function to format time stamps for bottom of chart
  269. * // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
  270. * scrollBackwards: false, // reverse the scroll direction of the chart
  271. * horizontalLines: [], // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
  272. * grid:
  273. * {
  274. * fillStyle: '#000000', // the background colour of the chart
  275. * lineWidth: 1, // the pixel width of grid lines
  276. * strokeStyle: '#777777', // colour of grid lines
  277. * millisPerLine: 1000, // distance between vertical grid lines
  278. * sharpLines: false, // controls whether grid lines are 1px sharp, or softened
  279. * verticalSections: 2, // number of vertical sections marked out by horizontal grid lines
  280. * borderVisible: true // whether the grid lines trace the border of the chart or not
  281. * },
  282. * labels
  283. * {
  284. * disabled: false, // enables/disables labels showing the min/max values
  285. * fillStyle: '#ffffff', // colour for text of labels,
  286. * fontSize: 15,
  287. * fontFamily: 'sans-serif',
  288. * precision: 2,
  289. * showIntermediateLabels: false, // shows intermediate labels between min and max values along y axis
  290. * intermediateLabelSameAxis: true,
  291. * },
  292. * title
  293. * {
  294. * text: '', // the text to display on the left side of the chart
  295. * fillStyle: '#ffffff', // colour for text
  296. * fontSize: 15,
  297. * fontFamily: 'sans-serif',
  298. * verticalAlign: 'middle' // one of 'top', 'middle', or 'bottom'
  299. * },
  300. * tooltip: false // show tooltip when mouse is over the chart
  301. * tooltipLine: { // properties for a vertical line at the cursor position
  302. * lineWidth: 1,
  303. * strokeStyle: '#BBBBBB'
  304. * },
  305. * tooltipFormatter: SmoothieChart.tooltipFormatter, // formatter function for tooltip text
  306. * nonRealtimeData: false, // use time of latest data as current time
  307. * displayDataFromPercentile: 1, // display not latest data, but data from the given percentile
  308. * // useful when trying to see old data saved by setting a high value for maxDataSetLength
  309. * // should be a value between 0 and 1
  310. * responsive: false, // whether the chart should adapt to the size of the canvas
  311. * limitFPS: 0 // maximum frame rate the chart will render at, in FPS (zero means no limit)
  312. * }
  313. * </pre>
  314. *
  315. * @constructor
  316. */
  317. function SmoothieChart(options) {
  318. this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options);
  319. this.seriesSet = [];
  320. this.currentValueRange = 1;
  321. this.currentVisMinValue = 0;
  322. this.lastRenderTimeMillis = 0;
  323. this.lastChartTimestamp = 0;
  324. this.mousemove = this.mousemove.bind(this);
  325. this.mouseout = this.mouseout.bind(this);
  326. }
  327. /** Formats the HTML string content of the tooltip. */
  328. SmoothieChart.tooltipFormatter = function (timestamp, data) {
  329. var timestampFormatter = this.options.timestampFormatter || SmoothieChart.timeFormatter,
  330. lines = [timestampFormatter(new Date(timestamp))],
  331. label;
  332. for (var i = 0; i < data.length; ++i) {
  333. label = data[i].series.options.tooltipLabel || ''
  334. if (label !== ''){
  335. label = label + ' ';
  336. }
  337. lines.push('<span style="color:' + data[i].series.options.strokeStyle + '">' +
  338. label +
  339. this.options.yMaxFormatter(data[i].value, this.options.labels.precision) + '</span>');
  340. }
  341. return lines.join('<br>');
  342. };
  343. SmoothieChart.defaultChartOptions = {
  344. millisPerPixel: 20,
  345. enableDpiScaling: true,
  346. yMinFormatter: function(min, precision) {
  347. return parseFloat(min).toFixed(precision);
  348. },
  349. yMaxFormatter: function(max, precision) {
  350. return parseFloat(max).toFixed(precision);
  351. },
  352. yIntermediateFormatter: function(intermediate, precision) {
  353. return parseFloat(intermediate).toFixed(precision);
  354. },
  355. maxValueScale: 1,
  356. minValueScale: 1,
  357. interpolation: 'bezier',
  358. scaleSmoothing: 0.125,
  359. maxDataSetLength: 2,
  360. scrollBackwards: false,
  361. displayDataFromPercentile: 1,
  362. grid: {
  363. fillStyle: '#000000',
  364. strokeStyle: '#777777',
  365. lineWidth: 1,
  366. sharpLines: false,
  367. millisPerLine: 1000,
  368. verticalSections: 2,
  369. borderVisible: true
  370. },
  371. labels: {
  372. fillStyle: '#ffffff',
  373. disabled: false,
  374. fontSize: 10,
  375. fontFamily: 'monospace',
  376. precision: 2,
  377. showIntermediateLabels: false,
  378. intermediateLabelSameAxis: true,
  379. },
  380. title: {
  381. text: '',
  382. fillStyle: '#ffffff',
  383. fontSize: 15,
  384. fontFamily: 'monospace',
  385. verticalAlign: 'middle'
  386. },
  387. horizontalLines: [],
  388. tooltip: false,
  389. tooltipLine: {
  390. lineWidth: 1,
  391. strokeStyle: '#BBBBBB'
  392. },
  393. tooltipFormatter: SmoothieChart.tooltipFormatter,
  394. nonRealtimeData: false,
  395. responsive: false,
  396. limitFPS: 0
  397. };
  398. // Based on http://inspirit.github.com/jsfeat/js/compatibility.js
  399. SmoothieChart.AnimateCompatibility = (function() {
  400. var requestAnimationFrame = function(callback, element) {
  401. var requestAnimationFrame =
  402. window.requestAnimationFrame ||
  403. window.webkitRequestAnimationFrame ||
  404. window.mozRequestAnimationFrame ||
  405. window.oRequestAnimationFrame ||
  406. window.msRequestAnimationFrame ||
  407. function(callback) {
  408. return window.setTimeout(function() {
  409. callback(Date.now());
  410. }, 16);
  411. };
  412. return requestAnimationFrame.call(window, callback, element);
  413. },
  414. cancelAnimationFrame = function(id) {
  415. var cancelAnimationFrame =
  416. window.cancelAnimationFrame ||
  417. function(id) {
  418. clearTimeout(id);
  419. };
  420. return cancelAnimationFrame.call(window, id);
  421. };
  422. return {
  423. requestAnimationFrame: requestAnimationFrame,
  424. cancelAnimationFrame: cancelAnimationFrame
  425. };
  426. })();
  427. SmoothieChart.defaultSeriesPresentationOptions = {
  428. lineWidth: 1,
  429. strokeStyle: '#ffffff'
  430. };
  431. /**
  432. * Adds a <code>TimeSeries</code> to this chart, with optional presentation options.
  433. *
  434. * Presentation options should be of the form (defaults shown):
  435. *
  436. * <pre>
  437. * {
  438. * lineWidth: 1,
  439. * strokeStyle: '#ffffff',
  440. * fillStyle: undefined,
  441. * tooltipLabel: undefined
  442. * }
  443. * </pre>
  444. */
  445. SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) {
  446. this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)});
  447. if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) {
  448. timeSeries.resetBoundsTimerId = setInterval(
  449. function() {
  450. timeSeries.resetBounds();
  451. },
  452. timeSeries.options.resetBoundsInterval
  453. );
  454. }
  455. };
  456. /**
  457. * Removes the specified <code>TimeSeries</code> from the chart.
  458. */
  459. SmoothieChart.prototype.removeTimeSeries = function(timeSeries) {
  460. // Find the correct timeseries to remove, and remove it
  461. var numSeries = this.seriesSet.length;
  462. for (var i = 0; i < numSeries; i++) {
  463. if (this.seriesSet[i].timeSeries === timeSeries) {
  464. this.seriesSet.splice(i, 1);
  465. break;
  466. }
  467. }
  468. // If a timer was operating for that timeseries, remove it
  469. if (timeSeries.resetBoundsTimerId) {
  470. // Stop resetting the bounds, if we were
  471. clearInterval(timeSeries.resetBoundsTimerId);
  472. }
  473. };
  474. /**
  475. * Gets render options for the specified <code>TimeSeries</code>.
  476. *
  477. * As you may use a single <code>TimeSeries</code> in multiple charts with different formatting in each usage,
  478. * these settings are stored in the chart.
  479. */
  480. SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) {
  481. // Find the correct timeseries to remove, and remove it
  482. var numSeries = this.seriesSet.length;
  483. for (var i = 0; i < numSeries; i++) {
  484. if (this.seriesSet[i].timeSeries === timeSeries) {
  485. return this.seriesSet[i].options;
  486. }
  487. }
  488. };
  489. /**
  490. * Brings the specified <code>TimeSeries</code> to the top of the chart. It will be rendered last.
  491. */
  492. SmoothieChart.prototype.bringToFront = function(timeSeries) {
  493. // Find the correct timeseries to remove, and remove it
  494. var numSeries = this.seriesSet.length;
  495. for (var i = 0; i < numSeries; i++) {
  496. if (this.seriesSet[i].timeSeries === timeSeries) {
  497. var set = this.seriesSet.splice(i, 1);
  498. this.seriesSet.push(set[0]);
  499. break;
  500. }
  501. }
  502. };
  503. /**
  504. * Instructs the <code>SmoothieChart</code> to start rendering to the provided canvas, with specified delay.
  505. *
  506. * @param canvas the target canvas element
  507. * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series
  508. * from appearing on screen, with new values flashing into view, at the expense of some latency.
  509. */
  510. SmoothieChart.prototype.streamTo = function(canvas, delayMillis) {
  511. this.canvas = canvas;
  512. this.delay = delayMillis;
  513. this.start();
  514. };
  515. SmoothieChart.prototype.getTooltipEl = function () {
  516. // Create the tool tip element lazily
  517. if (!this.tooltipEl) {
  518. this.tooltipEl = document.createElement('div');
  519. this.tooltipEl.className = 'smoothie-chart-tooltip';
  520. this.tooltipEl.style.position = 'absolute';
  521. this.tooltipEl.style.display = 'none';
  522. document.body.appendChild(this.tooltipEl);
  523. }
  524. return this.tooltipEl;
  525. };
  526. SmoothieChart.prototype.updateTooltip = function () {
  527. if(!this.options.tooltip){
  528. return;
  529. }
  530. var el = this.getTooltipEl();
  531. if (!this.mouseover || !this.options.tooltip) {
  532. el.style.display = 'none';
  533. return;
  534. }
  535. var time = this.lastChartTimestamp;
  536. // x pixel to time
  537. var t = this.options.scrollBackwards
  538. ? time - this.mouseX * this.options.millisPerPixel
  539. : time - (this.canvas.offsetWidth - this.mouseX) * this.options.millisPerPixel;
  540. var data = [];
  541. // For each data set...
  542. for (var d = 0; d < this.seriesSet.length; d++) {
  543. var timeSeries = this.seriesSet[d].timeSeries;
  544. if (timeSeries.disabled) {
  545. continue;
  546. }
  547. // find datapoint closest to time 't'
  548. var closeIdx = Util.binarySearch(timeSeries.data, t);
  549. if (closeIdx > 0 && closeIdx < timeSeries.data.length) {
  550. data.push({ series: this.seriesSet[d], index: closeIdx, value: timeSeries.data[closeIdx][1] });
  551. }
  552. }
  553. if (data.length) {
  554. el.innerHTML = this.options.tooltipFormatter.call(this, t, data);
  555. el.style.display = 'block';
  556. } else {
  557. el.style.display = 'none';
  558. }
  559. };
  560. SmoothieChart.prototype.mousemove = function (evt) {
  561. this.mouseover = true;
  562. this.mouseX = evt.offsetX;
  563. this.mouseY = evt.offsetY;
  564. this.mousePageX = evt.pageX;
  565. this.mousePageY = evt.pageY;
  566. if(!this.options.tooltip){
  567. return;
  568. }
  569. var el = this.getTooltipEl();
  570. el.style.top = Math.round(this.mousePageY) + 'px';
  571. el.style.left = Math.round(this.mousePageX) + 'px';
  572. this.updateTooltip();
  573. };
  574. SmoothieChart.prototype.mouseout = function () {
  575. this.mouseover = false;
  576. this.mouseX = this.mouseY = -1;
  577. if (this.tooltipEl)
  578. this.tooltipEl.style.display = 'none';
  579. };
  580. /**
  581. * Make sure the canvas has the optimal resolution for the device's pixel ratio.
  582. */
  583. SmoothieChart.prototype.resize = function () {
  584. var dpr = !this.options.enableDpiScaling || !window ? 1 : window.devicePixelRatio,
  585. width, height;
  586. if (this.options.responsive) {
  587. // Newer behaviour: Use the canvas's size in the layout, and set the internal
  588. // resolution according to that size and the device pixel ratio (eg: high DPI)
  589. width = this.canvas.offsetWidth;
  590. height = this.canvas.offsetHeight;
  591. if (width !== this.lastWidth) {
  592. this.lastWidth = width;
  593. this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
  594. this.canvas.getContext('2d').scale(dpr, dpr);
  595. }
  596. if (height !== this.lastHeight) {
  597. this.lastHeight = height;
  598. this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
  599. this.canvas.getContext('2d').scale(dpr, dpr);
  600. }
  601. } else if (dpr !== 1) {
  602. // Older behaviour: use the canvas's inner dimensions and scale the element's size
  603. // according to that size and the device pixel ratio (eg: high DPI)
  604. width = parseInt(this.canvas.getAttribute('width'));
  605. height = parseInt(this.canvas.getAttribute('height'));
  606. if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) {
  607. this.originalWidth = width;
  608. this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString());
  609. this.canvas.style.width = width + 'px';
  610. this.canvas.getContext('2d').scale(dpr, dpr);
  611. }
  612. if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) {
  613. this.originalHeight = height;
  614. this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString());
  615. this.canvas.style.height = height + 'px';
  616. this.canvas.getContext('2d').scale(dpr, dpr);
  617. }
  618. }
  619. };
  620. /**
  621. * Starts the animation of this chart.
  622. */
  623. SmoothieChart.prototype.start = function() {
  624. if (this.frame) {
  625. // We're already running, so just return
  626. return;
  627. }
  628. this.canvas.addEventListener('mousemove', this.mousemove);
  629. this.canvas.addEventListener('mouseout', this.mouseout);
  630. // Renders a frame, and queues the next frame for later rendering
  631. var animate = function() {
  632. this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() {
  633. if(this.options.nonRealtimeData){
  634. var dateZero = new Date(0);
  635. // find the data point with the latest timestamp
  636. var maxTimeStamp = this.seriesSet.reduce(function(max, series){
  637. var dataSet = series.timeSeries.data;
  638. var indexToCheck = Math.round(this.options.displayDataFromPercentile * dataSet.length) - 1;
  639. indexToCheck = indexToCheck >= 0 ? indexToCheck : 0;
  640. indexToCheck = indexToCheck <= dataSet.length -1 ? indexToCheck : dataSet.length -1;
  641. if(dataSet && dataSet.length > 0)
  642. {
  643. // timestamp corresponds to element 0 of the data point
  644. var lastDataTimeStamp = dataSet[indexToCheck][0];
  645. max = max > lastDataTimeStamp ? max : lastDataTimeStamp;
  646. }
  647. return max;
  648. }.bind(this), dateZero);
  649. // use the max timestamp as current time
  650. this.render(this.canvas, maxTimeStamp > dateZero ? maxTimeStamp : null);
  651. } else {
  652. this.render();
  653. }
  654. animate();
  655. }.bind(this));
  656. }.bind(this);
  657. animate();
  658. };
  659. /**
  660. * Stops the animation of this chart.
  661. */
  662. SmoothieChart.prototype.stop = function() {
  663. if (this.frame) {
  664. SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame);
  665. delete this.frame;
  666. this.canvas.removeEventListener('mousemove', this.mousemove);
  667. this.canvas.removeEventListener('mouseout', this.mouseout);
  668. }
  669. };
  670. SmoothieChart.prototype.updateValueRange = function() {
  671. // Calculate the current scale of the chart, from all time series.
  672. var chartOptions = this.options,
  673. chartMaxValue = Number.NaN,
  674. chartMinValue = Number.NaN;
  675. for (var d = 0; d < this.seriesSet.length; d++) {
  676. // TODO(ndunn): We could calculate / track these values as they stream in.
  677. var timeSeries = this.seriesSet[d].timeSeries;
  678. if (timeSeries.disabled) {
  679. continue;
  680. }
  681. if (!isNaN(timeSeries.maxValue)) {
  682. chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue;
  683. }
  684. if (!isNaN(timeSeries.minValue)) {
  685. chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue;
  686. }
  687. }
  688. // Scale the chartMaxValue to add padding at the top if required
  689. if (chartOptions.maxValue != null) {
  690. chartMaxValue = chartOptions.maxValue;
  691. } else {
  692. chartMaxValue *= chartOptions.maxValueScale;
  693. }
  694. // Set the minimum if we've specified one
  695. if (chartOptions.minValue != null) {
  696. chartMinValue = chartOptions.minValue;
  697. } else {
  698. chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue);
  699. }
  700. // If a custom range function is set, call it
  701. if (this.options.yRangeFunction) {
  702. var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue});
  703. chartMinValue = range.min;
  704. chartMaxValue = range.max;
  705. }
  706. if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) {
  707. var targetValueRange = chartMaxValue - chartMinValue;
  708. var valueRangeDiff = (targetValueRange - this.currentValueRange);
  709. var minValueDiff = (chartMinValue - this.currentVisMinValue);
  710. this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1;
  711. this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff;
  712. this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff;
  713. }
  714. this.valueRange = { min: chartMinValue, max: chartMaxValue };
  715. };
  716. SmoothieChart.prototype.render = function(canvas, time) {
  717. var nowMillis = Date.now();
  718. // Respect any frame rate limit.
  719. if (this.options.limitFPS > 0 && nowMillis - this.lastRenderTimeMillis < (1000/this.options.limitFPS))
  720. return;
  721. if (!this.isAnimatingScale) {
  722. // We're not animating. We can use the last render time and the scroll speed to work out whether
  723. // we actually need to paint anything yet. If not, we can return immediately.
  724. // Render at least every 1/6th of a second. The canvas may be resized, which there is
  725. // no reliable way to detect.
  726. var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel);
  727. if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) {
  728. return;
  729. }
  730. }
  731. this.resize();
  732. this.updateTooltip();
  733. this.lastRenderTimeMillis = nowMillis;
  734. canvas = canvas || this.canvas;
  735. time = time || nowMillis - (this.delay || 0);
  736. // Round time down to pixel granularity, so motion appears smoother.
  737. time -= time % this.options.millisPerPixel;
  738. this.lastChartTimestamp = time;
  739. var context = canvas.getContext('2d'),
  740. chartOptions = this.options,
  741. dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight },
  742. // Calculate the threshold time for the oldest data points.
  743. oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel),
  744. valueToYPixel = function(value) {
  745. var offset = value - this.currentVisMinValue;
  746. return this.currentValueRange === 0
  747. ? dimensions.height
  748. : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height));
  749. }.bind(this),
  750. timeToXPixel = function(t) {
  751. if(chartOptions.scrollBackwards) {
  752. return Math.round((time - t) / chartOptions.millisPerPixel);
  753. }
  754. return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel));
  755. };
  756. this.updateValueRange();
  757. context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily;
  758. // Save the state of the canvas context, any transformations applied in this method
  759. // will get removed from the stack at the end of this method when .restore() is called.
  760. context.save();
  761. // Move the origin.
  762. context.translate(dimensions.left, dimensions.top);
  763. // Create a clipped rectangle - anything we draw will be constrained to this rectangle.
  764. // This prevents the occasional pixels from curves near the edges overrunning and creating
  765. // screen cheese (that phrase should need no explanation).
  766. context.beginPath();
  767. context.rect(0, 0, dimensions.width, dimensions.height);
  768. context.clip();
  769. // Clear the working area.
  770. context.save();
  771. context.fillStyle = chartOptions.grid.fillStyle;
  772. context.clearRect(0, 0, dimensions.width, dimensions.height);
  773. context.fillRect(0, 0, dimensions.width, dimensions.height);
  774. context.restore();
  775. // Grid lines...
  776. context.save();
  777. context.lineWidth = chartOptions.grid.lineWidth;
  778. context.strokeStyle = chartOptions.grid.strokeStyle;
  779. // Vertical (time) dividers.
  780. if (chartOptions.grid.millisPerLine > 0) {
  781. context.beginPath();
  782. for (var t = time - (time % chartOptions.grid.millisPerLine);
  783. t >= oldestValidTime;
  784. t -= chartOptions.grid.millisPerLine) {
  785. var gx = timeToXPixel(t);
  786. if (chartOptions.grid.sharpLines) {
  787. gx -= 0.5;
  788. }
  789. context.moveTo(gx, 0);
  790. context.lineTo(gx, dimensions.height);
  791. }
  792. context.stroke();
  793. context.closePath();
  794. }
  795. // Horizontal (value) dividers.
  796. for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
  797. var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections);
  798. if (chartOptions.grid.sharpLines) {
  799. gy -= 0.5;
  800. }
  801. context.beginPath();
  802. context.moveTo(0, gy);
  803. context.lineTo(dimensions.width, gy);
  804. context.stroke();
  805. context.closePath();
  806. }
  807. // Bounding rectangle.
  808. if (chartOptions.grid.borderVisible) {
  809. context.beginPath();
  810. context.strokeRect(0, 0, dimensions.width, dimensions.height);
  811. context.closePath();
  812. }
  813. context.restore();
  814. // Draw any horizontal lines...
  815. if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) {
  816. for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) {
  817. var line = chartOptions.horizontalLines[hl],
  818. hly = Math.round(valueToYPixel(line.value)) - 0.5;
  819. context.strokeStyle = line.color || '#ffffff';
  820. context.lineWidth = line.lineWidth || 1;
  821. context.beginPath();
  822. context.moveTo(0, hly);
  823. context.lineTo(dimensions.width, hly);
  824. context.stroke();
  825. context.closePath();
  826. }
  827. }
  828. // For each data set...
  829. for (var d = 0; d < this.seriesSet.length; d++) {
  830. context.save();
  831. var timeSeries = this.seriesSet[d].timeSeries;
  832. if (timeSeries.disabled) {
  833. continue;
  834. }
  835. var dataSet = timeSeries.data,
  836. seriesOptions = this.seriesSet[d].options;
  837. // Delete old data that's moved off the left of the chart.
  838. timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength);
  839. // Set style for this dataSet.
  840. context.lineWidth = seriesOptions.lineWidth;
  841. context.strokeStyle = seriesOptions.strokeStyle;
  842. // Draw the line...
  843. context.beginPath();
  844. // Retain lastX, lastY for calculating the control points of bezier curves.
  845. var firstX = 0, firstY = 0, lastX = 0, lastY = 0;
  846. for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) {
  847. var x = timeToXPixel(dataSet[i][0]),
  848. y = valueToYPixel(dataSet[i][1]);
  849. if (i === 0) {
  850. firstX = x;
  851. firstY = y;
  852. context.moveTo(x, y);
  853. } else {
  854. switch (chartOptions.interpolation) {
  855. case "linear":
  856. case "line": {
  857. context.lineTo(x,y);
  858. break;
  859. }
  860. case "bezier":
  861. default: {
  862. // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves
  863. //
  864. // Assuming A was the last point in the line plotted and B is the new point,
  865. // we draw a curve with control points P and Q as below.
  866. //
  867. // A---P
  868. // |
  869. // |
  870. // |
  871. // Q---B
  872. //
  873. // Importantly, A and P are at the same y coordinate, as are B and Q. This is
  874. // so adjacent curves appear to flow as one.
  875. //
  876. context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop
  877. Math.round((lastX + x) / 2), lastY, // controlPoint1 (P)
  878. Math.round((lastX + x)) / 2, y, // controlPoint2 (Q)
  879. x, y); // endPoint (B)
  880. break;
  881. }
  882. case "step": {
  883. context.lineTo(x,lastY);
  884. context.lineTo(x,y);
  885. break;
  886. }
  887. }
  888. }
  889. lastX = x; lastY = y;
  890. }
  891. if (dataSet.length > 1) {
  892. if (seriesOptions.fillStyle) {
  893. // Close up the fill region.
  894. if (chartOptions.scrollBackwards) {
  895. context.lineTo(lastX, dimensions.height + seriesOptions.lineWidth);
  896. context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
  897. context.lineTo(firstX, firstY);
  898. } else {
  899. context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY);
  900. context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1);
  901. context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth);
  902. }
  903. context.fillStyle = seriesOptions.fillStyle;
  904. context.fill();
  905. }
  906. if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') {
  907. context.stroke();
  908. }
  909. context.closePath();
  910. }
  911. context.restore();
  912. }
  913. if (chartOptions.tooltip && this.mouseX >= 0) {
  914. // Draw vertical bar to show tooltip position
  915. context.lineWidth = chartOptions.tooltipLine.lineWidth;
  916. context.strokeStyle = chartOptions.tooltipLine.strokeStyle;
  917. context.beginPath();
  918. context.moveTo(this.mouseX, 0);
  919. context.lineTo(this.mouseX, dimensions.height);
  920. context.closePath();
  921. context.stroke();
  922. this.updateTooltip();
  923. }
  924. // Draw the axis values on the chart.
  925. if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) {
  926. var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision),
  927. minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision),
  928. maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2,
  929. minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2;
  930. context.fillStyle = chartOptions.labels.fillStyle;
  931. context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize);
  932. context.fillText(minValueString, minLabelPos, dimensions.height - 2);
  933. }
  934. // Display intermediate y axis labels along y-axis to the left of the chart
  935. if ( chartOptions.labels.showIntermediateLabels
  936. && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)
  937. && chartOptions.grid.verticalSections > 0) {
  938. // show a label above every vertical section divider
  939. var step = (this.valueRange.max - this.valueRange.min) / chartOptions.grid.verticalSections;
  940. var stepPixels = dimensions.height / chartOptions.grid.verticalSections;
  941. for (var v = 1; v < chartOptions.grid.verticalSections; v++) {
  942. var gy = dimensions.height - Math.round(v * stepPixels);
  943. if (chartOptions.grid.sharpLines) {
  944. gy -= 0.5;
  945. }
  946. var yValue = chartOptions.yIntermediateFormatter(this.valueRange.min + (v * step), chartOptions.labels.precision);
  947. //left of right axis?
  948. intermediateLabelPos =
  949. chartOptions.labels.intermediateLabelSameAxis
  950. ? (chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(yValue).width - 2)
  951. : (chartOptions.scrollBackwards ? dimensions.width - context.measureText(yValue).width - 2 : 0);
  952. context.fillText(yValue, intermediateLabelPos, gy - chartOptions.grid.lineWidth);
  953. }
  954. }
  955. // Display timestamps along x-axis at the bottom of the chart.
  956. if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) {
  957. var textUntilX = chartOptions.scrollBackwards
  958. ? context.measureText(minValueString).width
  959. : dimensions.width - context.measureText(minValueString).width + 4;
  960. for (var t = time - (time % chartOptions.grid.millisPerLine);
  961. t >= oldestValidTime;
  962. t -= chartOptions.grid.millisPerLine) {
  963. var gx = timeToXPixel(t);
  964. // Only draw the timestamp if it won't overlap with the previously drawn one.
  965. if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) {
  966. // Formats the timestamp based on user specified formatting function
  967. // SmoothieChart.timeFormatter function above is one such formatting option
  968. var tx = new Date(t),
  969. ts = chartOptions.timestampFormatter(tx),
  970. tsWidth = context.measureText(ts).width;
  971. textUntilX = chartOptions.scrollBackwards
  972. ? gx + tsWidth + 2
  973. : gx - tsWidth - 2;
  974. context.fillStyle = chartOptions.labels.fillStyle;
  975. if(chartOptions.scrollBackwards) {
  976. context.fillText(ts, gx, dimensions.height - 2);
  977. } else {
  978. context.fillText(ts, gx - tsWidth, dimensions.height - 2);
  979. }
  980. }
  981. }
  982. }
  983. // Display title.
  984. if (chartOptions.title.text !== '') {
  985. context.font = chartOptions.title.fontSize + 'px ' + chartOptions.title.fontFamily;
  986. var titleXPos = chartOptions.scrollBackwards ? dimensions.width - context.measureText(chartOptions.title.text).width - 2 : 2;
  987. if (chartOptions.title.verticalAlign == 'bottom') {
  988. context.textBaseline = 'bottom';
  989. var titleYPos = dimensions.height;
  990. } else if (chartOptions.title.verticalAlign == 'middle') {
  991. context.textBaseline = 'middle';
  992. var titleYPos = dimensions.height / 2;
  993. } else {
  994. context.textBaseline = 'top';
  995. var titleYPos = 0;
  996. }
  997. context.fillStyle = chartOptions.title.fillStyle;
  998. context.fillText(chartOptions.title.text, titleXPos, titleYPos);
  999. }
  1000. context.restore(); // See .save() above.
  1001. };
  1002. // Sample timestamp formatting function
  1003. SmoothieChart.timeFormatter = function(date) {
  1004. function pad2(number) { return (number < 10 ? '0' : '') + number }
  1005. return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds());
  1006. };
  1007. exports.TimeSeries = TimeSeries;
  1008. exports.SmoothieChart = SmoothieChart;
  1009. })(typeof exports === 'undefined' ? this : exports);