All Downloads are FREE. Search and download functionalities are using the official Maven repository.

webui.static.js.smoothie.js Maven / Gradle / Ivy

There is a newer version: 0.9.9
Show newest version

;(function(exports) {

  var Util = {
    extend: function() {
      arguments[0] = arguments[0] || {};
      for (var i = 1; i < arguments.length; i++)
      {
        for (var key in arguments[i])
        {
          if (arguments[i].hasOwnProperty(key))
          {
            if (typeof(arguments[i][key]) === 'object') {
              if (arguments[i][key] instanceof Array) {
                arguments[0][key] = arguments[i][key];
              } else {
                arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]);
              }
            } else {
              arguments[0][key] = arguments[i][key];
            }
          }
        }
      }
      return arguments[0];
    }
  };

  /**
   * Initialises a new TimeSeries with optional data options.
   *
   * Options are of the form (defaults shown):
   *
   * 
   * {
   *   resetBounds: true,        
   *   resetBoundsInterval: 3000 
   * }
   * 
* * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. * * @constructor */ function TimeSeries(options) { this.options = Util.extend({}, TimeSeries.defaultOptions, options); this.data = []; this.maxValue = Number.NaN; this.minValue = Number.NaN; } TimeSeries.defaultOptions = { resetBoundsInterval: 3000, resetBounds: false }; /** * Recalculate the min/max values for this TimeSeries object. * * This causes the graph to scale itself in the y-axis. */ TimeSeries.prototype.resetBounds = function() { if (this.data.length) { this.maxValue = this.data[0][1]; this.minValue = this.data[0][1]; for (var i = 1; i < this.data.length; i++) { var value = this.data[i][1]; if (value > this.maxValue) { this.maxValue = value; } if (value < this.minValue) { this.minValue = value; } } } else { this.maxValue = Number.NaN; this.minValue = Number.NaN; } }; /** * Adds a new data point to the TimeSeries, preserving chronological order. * * @param timestamp the position, in time, of this data point * @param value the value of this data point * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls * whether it is replaced, or the values summed (defaults to false.) */ TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { var i = this.data.length - 1; while (i > 0 && this.data[i][0] > timestamp) { i--; } if (this.data.length > 0 && this.data[i][0] === timestamp) { if (sumRepeatedTimeStampValues) { this.data[i][1] += value; value = this.data[i][1]; } else { this.data[i][1] = value; } } else if (i < this.data.length - 1) { this.data.splice(i + 1, 0, [timestamp, value]); } else { this.data.push([timestamp, value]); } this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); }; TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { var removeCount = 0; while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { removeCount++; } if (removeCount !== 0) { this.data.splice(0, removeCount); } }; /** * Initialises a new SmoothieChart. * * Options are optional, and should be of the form below. Just specify the values you * need and the rest will be given sensible defaults as shown: * *
   * {
   *   minValue: undefined,        
   *   maxValue: undefined,        
   *   maxValueScale: 1,           
   *   yRangeFunction: undefined,  
   *   scaleSmoothing: 0.125,      
   *   millisPerPixel: 20,         
   *   maxDataSetLength: 2,
   *   interpolation: 'bezier'     
   *   timestampFormatter: null,   
   *   horizontalLines: [],        
   *   grid:
   *   {
   *     fillStyle: '#000000',     
   *     lineWidth: 1,             
   *     strokeStyle: '#777777',   
   *     millisPerLine: 1000,      
   *     sharpLines: false,        
   *     verticalSections: 2,      
   *     borderVisible: true       
   *   },
   *   labels
   *   {
   *     disabled: false,          
   *     fillStyle: '#ffffff',     
   *     fontSize: 15,
   *     fontFamily: 'sans-serif',
   *     precision: 2
   *   },
   * }
   * 
* * @constructor */ function SmoothieChart(options) { this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); this.seriesSet = []; this.currentValueRange = 1; this.currentVisMinValue = 0; } SmoothieChart.defaultChartOptions = { millisPerPixel: 20, maxValueScale: 1, interpolation: 'bezier', scaleSmoothing: 0.125, maxDataSetLength: 2, grid: { fillStyle: '#000000', strokeStyle: '#777777', lineWidth: 1, sharpLines: false, millisPerLine: 1000, verticalSections: 2, borderVisible: true }, labels: { fillStyle: '#ffffff', disabled: false, fontSize: 10, fontFamily: 'monospace', precision: 2 }, horizontalLines: [] }; SmoothieChart.AnimateCompatibility = (function() { var lastTime = 0, requestAnimationFrame = function(callback, element) { var requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) { var currTime = new Date().getTime(), timeToCall = Math.max(0, 16 - (currTime - lastTime)), id = window.setTimeout(function() { callback(currTime + timeToCall); }, timeToCall); lastTime = currTime + timeToCall; return id; }; return requestAnimationFrame.call(window, callback, element); }, cancelAnimationFrame = function(id) { var cancelAnimationFrame = window.cancelAnimationFrame || function(id) { clearTimeout(id); }; return cancelAnimationFrame.call(window, id); }; return { requestAnimationFrame: requestAnimationFrame, cancelAnimationFrame: cancelAnimationFrame }; })(); SmoothieChart.defaultSeriesPresentationOptions = { lineWidth: 1, strokeStyle: '#ffffff' }; /** * Adds a TimeSeries to this chart, with optional presentation options. * * Presentation options should be of the form (defaults shown): * *
   * {
   *   lineWidth: 1,
   *   strokeStyle: '#ffffff',
   *   fillStyle: undefined
   * }
   * 
*/ SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { timeSeries.resetBoundsTimerId = setInterval( function() { timeSeries.resetBounds(); }, timeSeries.options.resetBoundsInterval ); } }; /** * Removes the specified TimeSeries from the chart. */ SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { var numSeries = this.seriesSet.length; for (var i = 0; i < numSeries; i++) { if (this.seriesSet[i].timeSeries === timeSeries) { this.seriesSet.splice(i, 1); break; } } if (timeSeries.resetBoundsTimerId) { clearInterval(timeSeries.resetBoundsTimerId); } }; /** * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. * * @param canvas the target canvas element * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series * from appearing on screen, with new values flashing into view, at the expense of some latency. */ SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { this.canvas = canvas; this.delay = delayMillis; this.start(); }; /** * Starts the animation of this chart. */ SmoothieChart.prototype.start = function() { if (this.frame) { return; } var animate = function() { this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { this.render(); animate(); }.bind(this)); }.bind(this); animate(); }; /** * Stops the animation of this chart. */ SmoothieChart.prototype.stop = function() { if (this.frame) { SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); delete this.frame; } }; SmoothieChart.prototype.updateValueRange = function() { var chartOptions = this.options, chartMaxValue = Number.NaN, chartMinValue = Number.NaN; for (var d = 0; d < this.seriesSet.length; d++) { var timeSeries = this.seriesSet[d].timeSeries; if (!isNaN(timeSeries.maxValue)) { chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; } if (!isNaN(timeSeries.minValue)) { chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; } } if (chartOptions.maxValue != null) { chartMaxValue = chartOptions.maxValue; } else { chartMaxValue *= chartOptions.maxValueScale; } if (chartOptions.minValue != null) { chartMinValue = chartOptions.minValue; } if (this.options.yRangeFunction) { var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); chartMinValue = range.min; chartMaxValue = range.max; } if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { var targetValueRange = chartMaxValue - chartMinValue; this.currentValueRange += chartOptions.scaleSmoothing * (targetValueRange - this.currentValueRange); this.currentVisMinValue += chartOptions.scaleSmoothing * (chartMinValue - this.currentVisMinValue); } this.valueRange = { min: chartMinValue, max: chartMaxValue }; }; SmoothieChart.prototype.render = function(canvas, time) { canvas = canvas || this.canvas; time = time || new Date().getTime() - (this.delay || 0); time -= time % this.options.millisPerPixel; var context = canvas.getContext('2d'), chartOptions = this.options, dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), valueToYPixel = function(value) { var offset = value - this.currentVisMinValue; return this.currentValueRange === 0 ? dimensions.height : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); }.bind(this), timeToXPixel = function(t) { return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); }; this.updateValueRange(); context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; context.save(); context.translate(dimensions.left, dimensions.top); context.beginPath(); context.rect(0, 0, dimensions.width, dimensions.height); context.clip(); context.save(); context.fillStyle = chartOptions.grid.fillStyle; context.clearRect(0, 0, dimensions.width, dimensions.height); context.fillRect(0, 0, dimensions.width, dimensions.height); context.restore(); context.save(); context.lineWidth = chartOptions.grid.lineWidth; context.strokeStyle = chartOptions.grid.strokeStyle; if (chartOptions.grid.millisPerLine > 0) { var textUntilX = dimensions.width - context.measureText(minValueString).width + 4; for (var t = time - (time % chartOptions.grid.millisPerLine); t >= oldestValidTime; t -= chartOptions.grid.millisPerLine) { var gx = timeToXPixel(t); if (chartOptions.grid.sharpLines) { gx -= 0.5; } context.beginPath(); context.moveTo(gx, 0); context.lineTo(gx, dimensions.height); context.stroke(); context.closePath(); if (chartOptions.timestampFormatter && gx < textUntilX) { var tx = new Date(t), ts = chartOptions.timestampFormatter(tx), tsWidth = context.measureText(ts).width; textUntilX = gx - tsWidth - 2; context.fillStyle = chartOptions.labels.fillStyle; context.fillText(ts, gx - tsWidth, dimensions.height - 2); } } } for (var v = 1; v < chartOptions.grid.verticalSections; v++) { var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); if (chartOptions.grid.sharpLines) { gy -= 0.5; } context.beginPath(); context.moveTo(0, gy); context.lineTo(dimensions.width, gy); context.stroke(); context.closePath(); } if (chartOptions.grid.borderVisible) { context.beginPath(); context.strokeRect(0, 0, dimensions.width, dimensions.height); context.closePath(); } context.restore(); if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { var line = chartOptions.horizontalLines[hl], hly = Math.round(valueToYPixel(line.value)) - 0.5; context.strokeStyle = line.color || '#ffffff'; context.lineWidth = line.lineWidth || 1; context.beginPath(); context.moveTo(0, hly); context.lineTo(dimensions.width, hly); context.stroke(); context.closePath(); } } for (var d = 0; d < this.seriesSet.length; d++) { context.save(); var timeSeries = this.seriesSet[d].timeSeries, dataSet = timeSeries.data, seriesOptions = this.seriesSet[d].options; timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); context.lineWidth = seriesOptions.lineWidth; context.strokeStyle = seriesOptions.strokeStyle; context.beginPath(); var firstX = 0, lastX = 0, lastY = 0; for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { var x = timeToXPixel(dataSet[i][0]), y = valueToYPixel(dataSet[i][1]); if (i === 0) { firstX = x; context.moveTo(x, y); } else { switch (chartOptions.interpolation) { case "linear": case "line": { context.lineTo(x,y); break; } case "bezier": default: { context.bezierCurveTo( Math.round((lastX + x) / 2), lastY, Math.round((lastX + x)) / 2, y, x, y); break; } } } lastX = x; lastY = y; } if (dataSet.length > 1) { if (seriesOptions.fillStyle) { context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); context.fillStyle = seriesOptions.fillStyle; context.fill(); } if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { context.stroke(); } context.closePath(); } context.restore(); } if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { var maxValueString = parseFloat(this.valueRange.max).toFixed(chartOptions.labels.precision), minValueString = parseFloat(this.valueRange.min).toFixed(chartOptions.labels.precision); context.fillStyle = chartOptions.labels.fillStyle; context.fillText(maxValueString, dimensions.width - context.measureText(maxValueString).width - 2, chartOptions.labels.fontSize); context.fillText(minValueString, dimensions.width - context.measureText(minValueString).width - 2, dimensions.height - 2); } context.restore(); }; SmoothieChart.timeFormatter = function(date) { function pad2(number) { return (number < 10 ? '0' : '') + number } return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); }; exports.TimeSeries = TimeSeries; exports.SmoothieChart = SmoothieChart; })(typeof exports === 'undefined' ? this : exports);




© 2015 - 2025 Weber Informatics LLC | Privacy Policy