//======================================================================================================================
// Class for making it easier to work with the Bodytrack Grapher.
//
// Dependencies:
// * jQuery (http://jquery.com/)
// * One of the following:
// * The CREATE Lab Grapher (https://github.com/CMU-CREATE-Lab/grapher)
// * The Bodytrack Grapher (https://github.com/BodyTrack/Grapher)
//
// Author: Chris Bartley (bartley@cmu.edu)
//======================================================================================================================
//======================================================================================================================
// VERIFY NAMESPACE
//======================================================================================================================
// Create the global symbol "org" if it doesn't exist. Throw an error if it does exist but is not an object.
var org;
if (!org) {
/** @namespace */
org = {};
}
else {
if (typeof org != "object") {
var orgExistsMessage = "Error: failed to create org namespace: org already exists and is not an object";
alert(orgExistsMessage);
throw new Error(orgExistsMessage);
}
}
// Repeat the creation and type-checking for the next level
if (!org.bodytrack) {
/** @namespace */
org.bodytrack = {};
}
else {
if (typeof org.bodytrack != "object") {
var orgBodytrackExistsMessage = "Error: failed to create org.bodytrack namespace: org.bodytrack already exists and is not an object";
alert(orgBodytrackExistsMessage);
throw new Error(orgBodytrackExistsMessage);
}
}
// Repeat the creation and type-checking for the next level
if (!org.bodytrack.grapher) {
/** @namespace */
org.bodytrack.grapher = {};
}
else {
if (typeof org.bodytrack.grapher != "object") {
var orgBodytrackGrapherExistsMessage = "Error: failed to create org.bodytrack.grapher namespace: org.bodytrack.grapher already exists and is not an object";
alert(orgBodytrackGrapherExistsMessage);
throw new Error(orgBodytrackGrapherExistsMessage);
}
}
//======================================================================================================================
//======================================================================================================================
// DEPENDECIES
//======================================================================================================================
if (!window['$']) {
var nojQueryMsg = "The jQuery library is required by org.bodytrack.grapher.PlotManager.js";
alert(nojQueryMsg);
throw new Error(nojQueryMsg);
}
//======================================================================================================================
//======================================================================================================================
// CODE
//======================================================================================================================
(function() {
// Got this from http://stackoverflow.com/a/9436948/703200
var isString = function(o) {
return (typeof o == 'string' || o instanceof String)
};
// Got this from http://stackoverflow.com/a/9716488/703200
var isNumeric = function(n) {
return !isNaN(parseFloat(n)) && isFinite(n);
};
var isNumberOrString = function(o) {
return isString(o) || isNumeric(o);
};
/**
* The function which the datasource function will call upon success, giving it the tile JSON. Some implementations
* can handle either an object or a string, some require one or the other.
*
* @callback datasourceSuccessCallbackFunction
* @param {object|string} json - the tile JSON, as either an object or a string
*/
/**
* Datasource function with signature <code>function(level, offset, successCallback)</code> resposible for
* returning tile JSON for the given <code>level</code> and <code>offset</code>.
*
* @callback datasourceFunction
* @param {number} level - the tile's level
* @param {number} offset - the tile's offset
* @param {datasourceSuccessCallbackFunction} successCallback - success callback function which expects to be given the tile JSON
*/
/**
* The min and max values for an axis's range.
*
* @typedef {Object} AxisRange
* @property {number} min - the range min
* @property {number} max - the range max
*/
/**
* Strategy function which pads the given <code>AxisRange</code> and returns a new <code>AxisRange</code>.
*
* @callback yAxisRangePaddingStrategyFunction
* @param {AxisRange} range an axis range
* @returns {AxisRange} the padded axis range
*/
/**
* An axis change event object.
*
* @typedef {Object} AxisChangeEvent
* @property {number} min - the axis's min value
* @property {number} max - the axis's max value
* @property {number|null} cursorPosition - the value of the cursor
* @property {string|null} cursorPositionString - the value of the cursor, expressed as a string
*/
/**
* Function for handling axis change events. Note that the event object passed to the function may be
* <code>null</code>.
*
* @callback axisChangeListenerFunction
* @param {AxisChangeEvent|null} axisChangeEvent - the <code>{@link AxisChangeEvent}</code>, may be <code>null</code>
*/
/**
* The statistics about plot data within a specific time range.
*
* @typedef {Object} PlotStatistics
* @property {number|null} count - number of data points in the range, may be <code>null</code> if unknown
* @property {boolean} hasData - whether there are any data points in the range
* @property {number|null} minValue - the minimum Y value in the range, or <code>null</code> (or not present in the object) if there is no data within the range.
* @property {number|null} maxValue - the maximum Y value in the range, or <code>null</code> (or not present in the object) if there is no data within the range.
*/
/**
*
* @param {AxisRange|number} rangeOrMin - an {@link AxisRange} or a double representing the axis min. If
* <code>null</code>, undefined, non-numeric, or not an <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code>
* is used instead.
* @param {number} max - a double representing the axis max. If <code>null</code>, undefined, or non-numeric, then
* <code>Number.MAX_VALUE</code> is used instead.
* @returns {AxisRange}
*/
var validateAxisRange = function(rangeOrMin, max) {
var min = rangeOrMin;
if (typeof rangeOrMin == 'object' && rangeOrMin != null) {
min = rangeOrMin.min;
max = rangeOrMin.max;
}
return {
min : isNumeric(min) ? min : -1 * Number.MAX_VALUE,
max : isNumeric(max) ? max : Number.MAX_VALUE
}
};
/**
* Returns the item in the given <code>hash</code> associated with the given <code>keyToFind</code>. If no such
* item exists, it returns <code>null</code>. If the <code>keyToFind</code> undefined or <code>null</code>, this
* method returns the first item found in the hash. If the hash is empty, it returns <code>null</code>.
*
* @private
* @param {Object} hash - the hash
* @param {string|number} [keyToFind] - the key of the item to find in the hash
* @returns {*|null}
*/
var getItemFromHashOrFindFirst = function(hash, keyToFind) {
if (typeof hash !== 'undefined' && hash != null) {
// if the keyToFind is undefined or null, just return the first item in the hash, if any
if (typeof keyToFind === 'undefined' || keyToFind == null) {
var keys = Object.keys(hash);
if (keys.length > 0) {
return hash[keys[0]];
}
}
else {
if (keyToFind in hash) {
return hash[keyToFind];
}
}
}
return null;
};
/**
* Wrapper class to make it easier to work with a date axis.
*
* @class
* @constructor
* @param {string} elementId - the DOM element ID for the container div holding the date axis
*/
org.bodytrack.grapher.DateAxis = function(elementId) {
var self = this;
var wrappedAxis = null;
/**
* Returns the DOM element ID for the container div holding this axis.
*
* @returns {string}
*/
this.getElementId = function() {
return elementId;
};
/**
* Returns the wrapped <code>DateAxis</code> object.
*
* @returns {DateAxis}
*/
this.getWrappedAxis = function() {
return wrappedAxis;
};
/**
* Adds the given function as an axis change listener. Does nothing if the given <code>listener</code> is not a
* function.
*
* @param {axisChangeListenerFunction} listener - function for handling an <code>{@link AxisChangeEvent}</code>.
*/
this.addAxisChangeListener = function(listener) {
if (typeof listener === 'function') {
wrappedAxis.addAxisChangeListener(listener);
}
};
/**
* Removes the given axis change listener. Does nothing if the given <code>listener</code> is not a function.
*
* @param {axisChangeListenerFunction} listener - function for handling an <code>{@link AxisChangeEvent}</code>.
*/
this.removeAxisChangeListener = function(listener) {
if (typeof listener === 'function') {
wrappedAxis.removeAxisChangeListener(listener);
}
};
/**
* Returns the date axis's current range as and object containing <code>mine</code> and <code>max</code> fields.
*
* @returns {AxisRange}
*/
this.getRange = function() {
return {
min : wrappedAxis.getMin(),
max : wrappedAxis.getMax()
};
};
/**
* Returns the current cursor position.
*
* @returns {number}
*/
this.getCursorPosition = function() {
return wrappedAxis.getCursorPosition();
};
/**
* Sets the cursor position to the given <code>timeInSecs</code>. The cursor is hidden if <code>timeInSecs</code>
* is <code>null</code> or undefined.
*
* @param {number} timeInSecs - the time at which the cursor should be placed.
*/
this.setCursorPosition = function(timeInSecs) {
wrappedAxis.setCursorPosition(timeInSecs);
};
/**
* Hides the cursor. This is merely a helper method, identical to calling
* <code>{@link #setCursorPosition setCursorPosition(null)}</code>.
*/
this.hideCursor = function() {
wrappedAxis.setCursorPosition(null);
};
/**
* Sets the cursor to the color described by the given <code>colorDescriptor</code>, or to black if the given
* <code>colorDescriptor</code> is undefined, <code>null</code>, or invalid. The color descriptor can be any valid
* CSS color descriptor such as a word ("green", "blue", etc.), a hex color (e.g. "#ff0000"), or an RGB color
* (e.g. "rgb(255,0,0)" or "rgba(0,255,0,0.5)").
*
* @param {string} colorDescriptor - a string description of the desired color.
*/
this.setCursorColor = function(colorDescriptor) {
wrappedAxis.setCursorColor(colorDescriptor);
};
/**
* Sets the visible range of the axis.
*
* @param {AxisRange|number} rangeOrMinTimeSecs - an {@link AxisRange} or a double representing the time in Unix
* time seconds of the start of the visible time range. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number} [maxTimeSecs] - a double representing the time in Unix time seconds of the end of the visible
* time range. If <code>null</code>, undefined, or non-numeric, then <code>Number.MAX_VALUE</code> is used
* instead.
*/
this.setRange = function(rangeOrMinTimeSecs, maxTimeSecs) {
var validRange = validateAxisRange(rangeOrMinTimeSecs, maxTimeSecs);
wrappedAxis.setRange(validRange.min, validRange.max);
};
/**
* Constrains the range of the axis so that the user cannot pan/zoom outside the specified range.
*
* @param {AxisRange|number} rangeOrMinTimeSecs - an {@link AxisRange} a double representing the minimum time in
* Unix time seconds of the time range. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number} [maxTimeSecs] - a double representing the maximum time in Unix time seconds of the time range. If
* <code>null</code>, undefined, or non-numeric, then <code>Number.MAX_VALUE</code> is used instead.
*/
this.constrainRangeTo = function(rangeOrMinTimeSecs, maxTimeSecs) {
var validRange = validateAxisRange(rangeOrMinTimeSecs, maxTimeSecs);
wrappedAxis.setMaxRange(validRange.min, validRange.max);
};
/**
* Clears the range constraints by setting bounds to [<code>-1 * Number.MAX_VALUE</code>, <code>Number.MAX_VALUE</code>].
*/
this.clearRangeConstraints = function() {
self.constrainRangeTo(null, null)
};
/**
* Constrains the range of the axis so that the user cannot pan/zoom deeper than the specified range.
*
* @param {AxisRange|number|null} rangeOrMinTimeSecs - an {@link AxisRange} a double representing the minimum time
* in Unix time seconds of the time range. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number|null} [maxTimeSecs] - a double representing the maximum time in Unix time seconds of the time
* range. If <code>null</code>, undefined, or non-numeric, then <code>Number.MAX_VALUE</code> is used instead.
*/
this.constrainMinRangeTo = function(rangeOrMinTimeSecs, maxTimeSecs) {
var validRange = validateAxisRange(rangeOrMinTimeSecs, maxTimeSecs);
wrappedAxis.setMinRangeConstraints(validRange.min, validRange.max);
};
/**
* Clears the min range constraints by setting bounds to [<code>-1 * Number.MAX_VALUE</code>,
* <code>Number.MAX_VALUE</code>].
*/
this.clearMinRangeConstraints = function() {
self.constrainMinRangeTo(null, null);
};
/**
* Sets the width of the axis.
*
* @param {int} width - the new width
*/
this.setWidth = function(width) {
var element = $("#" + elementId);
element.width(width);
wrappedAxis.setSize(width, element.height(), SequenceNumber.getNext());
};
/**
* Returns the width of the DateAxis's container div.
*
* @returns {int} the width of the DateAxis's container div
*/
this.getWidth = function() {
return $("#" + elementId).width();
};
// the "constructor"
(function() {
wrappedAxis = new DateAxis(elementId, "horizontal");
// Tell the DateAxis instance the size of its container div
var dateAxisElement = $("#" + elementId);
wrappedAxis.setSize(dateAxisElement.width(), dateAxisElement.height(), SequenceNumber.getNext());
})();
};
/**
* Wrapper class to make it easier to work with a Y axis.
*
* @class
* @constructor
* @param {string} elementId - the DOM element ID for the container div holding this Y axis
* @param {number} [yMin=0] - the minimum initial value for the Y axis. Defaults to 0 if undefined, <code>null</code>, or non-numeric.
* @param {number} [yMax=100] - the maximum initial value for the Y axis. Defaults to 100 if undefined, <code>null</code>, or non-numeric.
* @param {boolean} [willNotPadRange=false] - whether to pad the range
* @param {yAxisRangePaddingStrategyFunction} [rangePaddingStrategyFunction] - range padding strategy function (only used if padding is enabled).
*/
org.bodytrack.grapher.YAxis = function(elementId, yMin, yMax, willNotPadRange, rangePaddingStrategyFunction) {
var self = this;
var wrappedAxis = null;
/**
* Returns the DOM element ID for the container div holding this axis.
*
* @returns {string}
*/
this.getElementId = function() {
return elementId;
};
/**
* Returns the wrapped <code>DateAxis</code> object.
*
* @returns {DateAxis}
*/
this.getWrappedAxis = function() {
return wrappedAxis
};
/**
* Adds the given function as an axis change listener. Does nothing if the given <code>listener</code> is not a
* function.
*
* @param {axisChangeListenerFunction} listener - function for handling an <code>{@link AxisChangeEvent}</code>.
*/
this.addAxisChangeListener = function(listener) {
if (typeof listener === 'function') {
wrappedAxis.addAxisChangeListener(listener);
}
};
/**
* Removes the given axis change listener. Does nothing if the given <code>listener</code> is not a function.
*
* @param {axisChangeListenerFunction} listener - function for handling an <code>{@link AxisChangeEvent}</code>.
*/
this.removeAxisChangeListener = function(listener) {
if (typeof listener === 'function') {
wrappedAxis.removeAxisChangeListener(listener);
}
};
/**
* Returns the Y axis's current range as and object containing <code>mine</code> and <code>max</code> fields.
*
* @returns {AxisRange}
*/
this.getRange = function() {
if (typeof wrappedAxis.getRange === 'function') {
return wrappedAxis.getRange();
}
else {
return {
min : wrappedAxis.getMin(),
max : wrappedAxis.getMax()
};
}
};
/**
* Sets the visible range of the axis.
*
* @param {AxisRange|number} rangeOrMin - an {@link AxisRange} a double representing the minimum value. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number} max - the max value. If <code>null</code>, undefined, or non-numeric, then
* <code>Number.MAX_VALUE</code> is used instead. This argument is ignored (but required) if the first argument
* is an {@link AxisRange}.
* @param {boolean} [willNotPad=false] - whether to pad the range
*/
this.setRange = function(rangeOrMin, max, willNotPad) {
var range = validateAxisRange(rangeOrMin, max);
// pad, if desired
if (!willNotPad) {
range = rangePaddingStrategy(range);
}
wrappedAxis.setRange(range.min, range.max);
};
/**
* Constrains the range of the axis so that the user cannot pan/zoom outside the specified range.
*
* @param {AxisRange|number} rangeOrMin - an {@link AxisRange} a double representing the minimum value. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number} max - the max value. If <code>null</code>, undefined, or non-numeric, then
* <code>Number.MAX_VALUE</code> is used instead. This argument is ignored (but required) if the first argument
* is an {@link AxisRange}.
* @param {boolean} [willNotPad=false] - whether to pad the range
*/
this.constrainRangeTo = function(rangeOrMin, max, willNotPad) {
var range = validateAxisRange(rangeOrMin, max);
// pad, if desired
if (!willNotPad) {
range = rangePaddingStrategy(range);
}
wrappedAxis.setMaxRange(range.min, range.max);
};
/**
* Clears the range constraints by setting bounds to [<code>-1 * Number.MAX_VALUE</code>, <code>Number.MAX_VALUE</code>].
*/
this.clearRangeConstraints = function() {
self.constrainRangeTo(null, null);
};
/**
* Constrains the range of the axis so that the user cannot pan/zoom deeper than the specified range.
*
* @param {AxisRange|number|null} rangeOrMin - an {@link AxisRange} a double representing the minimum value. If
* <code>null</code>, undefined, non-numeric, or not an <code>AxisRange</code>, then
* <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number|null} max - the max value. If <code>null</code>, undefined, or non-numeric, then
* <code>Number.MAX_VALUE</code> is used instead. This argument is ignored (but required) if the first argument
* is an {@link AxisRange}.
* @param {boolean} [willNotPad=false] - whether to pad the range
*/
this.constrainMinRangeTo = function(rangeOrMin, max, willNotPad) {
var range = validateAxisRange(rangeOrMin, max);
// pad, if desired
if (!willNotPad) {
range = rangePaddingStrategy(range);
}
wrappedAxis.setMinRangeConstraints(range.min, range.max);
};
/**
* Clears the range constraints by setting bounds to [<code>-1 * Number.MAX_VALUE</code>,
* <code>Number.MAX_VALUE</code>].
*/
this.clearMinRangeConstraints = function() {
self.constrainMinRangeTo(null, null);
};
/**
* Sets the height of the axis.
*
* @param {int} height - the new height
*/
this.setHeight = function(height) {
var element = $("#" + elementId);
element.height(height);
wrappedAxis.setSize(element.width(), height, SequenceNumber.getNext());
};
var __defaultRangePaddingStrategy = function(range) {
var paddedRange = {
min : range.min,
max : range.max
};
var yDiff = paddedRange.max - paddedRange.min;
if (isFinite(yDiff)) {
var padding;
if (yDiff < 1e-10) {
padding = 0.5;
}
else {
padding = 0.05 * yDiff;
}
paddedRange.min -= padding;
paddedRange.max += padding;
}
return paddedRange;
};
var rangePaddingStrategy = __defaultRangePaddingStrategy;
/**
* Sets the strategy function for padding the range, if padding is enabled. If <code>f</code> is not a function,
* then the padding strategy reverts to the default.
*
* @param {yAxisRangePaddingStrategyFunction} f - the range padding strategy function
*/
this.setRangePaddingStrategy = function(f) {
if (typeof f === 'function') {
rangePaddingStrategy = f;
}
else {
rangePaddingStrategy = __defaultRangePaddingStrategy;
}
};
// the "constructor"
(function() {
var range = {
min : isNumeric(yMin) ? yMin : 0,
max : isNumeric(yMax) ? yMax : 100
};
self.setRangePaddingStrategy(rangePaddingStrategyFunction);
// pad, if desired and possible
if (!willNotPadRange) {
range = rangePaddingStrategy(range);
}
wrappedAxis = new NumberAxis(elementId, "vertical", range);
// set the size
var element = $("#" + elementId);
wrappedAxis.setSize(element.width(), element.height(), SequenceNumber.getNext());
})();
};
/**
* Wrapper class to make it easier to work with a DataSeriesPlot.
*
* @class
* @constructor
* @param {string|number} plotId - A identifier for this plot, unique within the PlotContainer. Must be a number or a string.
* @param {datasourceFunction} datasource - function with signature <code>function(level, offset, successCallback)</code> resposible for returning tile JSON for the given <code>level</code> and <code>offset</code>
* @param {org.bodytrack.grapher.DateAxis} dateAxis - the date axis
* @param {org.bodytrack.grapher.YAxis} yAxis - the Y axis
* @param {Object} [style] - the style object. A default style is used if undefined, null, or not an object.
* @param {boolean} [isLocalTime=false] - whether the plot's data uses local time. Defaults to false (UTC).
*/
org.bodytrack.grapher.DataSeriesPlot = function(plotId, datasource, dateAxis, yAxis, style, isLocalTime) {
var self = this;
var DEFAULT_STYLE = {
"styles" : [
{ "type" : "line", "lineWidth" : 1, "show" : true, "color" : "black" },
{ "type" : "circle", "radius" : 2, "lineWidth" : 1, "show" : true, "color" : "black", "fill" : true }
],
"highlight" : {
"lineWidth" : 1,
"styles" : [
{
"type" : "circle",
"radius" : 3,
"lineWidth" : 0.5,
"show" : true,
"color" : "#ff0000",
"fill" : true
},
{
"show" : true,
"type" : "value",
"fillColor" : "#000000",
"marginWidth" : 10,
"font" : "7pt Helvetica,Arial,Verdana,sans-serif",
"verticalOffset" : 7,
"numberFormat" : "###,##0.#"
}
]
}
};
var wrappedPlot = null;
/**
* Returns the plot's ID.
*
* @returns {string|number}
*/
this.getId = function() {
return plotId;
};
/**
* Returns the wrapped <code>DataSeriesPlot</code> object.
*
* @returns {DataSeriesPlot}
*/
this.getWrappedPlot = function() {
return wrappedPlot;
};
/**
* Gets statistcs about the data within the specified time range. Note that some implementations may limit the
* time range for the statistics to the current visible date range.
*
* @param {AxisRange|number} rangeOrMinTimeSecs - an {@link AxisRange} or a double representing the time in Unix
* time seconds of the start of the visible time range. If <code>null</code>, undefined, non-numeric, or not an
* <code>AxisRange</code>, then <code>-1*Number.MAX_VALUE</code> is used instead.
* @param {number} [maxTimeSecs] - a double representing the time in Unix time seconds of the end of the visible
* time range. If <code>null</code>, undefined, or non-numeric, then <code>Number.MAX_VALUE</code> is used
* instead.
* @returns {PlotStatistics}
*/
this.getStatisticsWithinRange = function(rangeOrMinTimeSecs, maxTimeSecs) {
var validRange = validateAxisRange(rangeOrMinTimeSecs, maxTimeSecs);
var rawStatistics;
if (typeof wrappedPlot.getSimpleStatistics === 'function') {
rawStatistics = wrappedPlot.getSimpleStatistics(validRange.min, validRange.max);
}
else if (typeof wrappedPlot.getMinMaxValuesWithinTimeRange === 'function') {
var result = wrappedPlot.getMinMaxValuesWithinTimeRange(validRange.min, validRange.max);
rawStatistics = {
count : null,
y_min : result ? result.min : null,
y_max : result ? result.max : null,
has_data : !!(result && result.min != null && result.max != null)
}
}
return {
count : rawStatistics['count'],
hasData : rawStatistics['has_data'],
minValue : rawStatistics['has_data'] ? rawStatistics['y_min'] : null,
maxValue : rawStatistics['has_data'] ? rawStatistics['y_max'] : null
};
};
/**
* Sets the plot's cursor to the color described by the given <code>colorDescriptor</code>, or to black if the
* given <code>colorDescriptor</code> is undefined, <code>null</code>, or invalid. The color descriptor can be any
* valid CSS color descriptor such as a word ("green", "blue", etc.), a hex color (e.g. "#ff0000"), or an RGB
* color (e.g. "rgb(255,0,0)" or "rgba(0,255,0,0.5)").
*
* @param {string} colorDescriptor - a string description of the desired color.
*/
this.setCursorColor = function(colorDescriptor) {
var theStyle = self.getStyle();
if (!("cursor" in theStyle)) {
theStyle["cursor"] = { color : null };
}
theStyle["cursor"]['color'] = colorDescriptor;
self.setStyle(theStyle);
};
/**
* A data point object.
*
* @typedef {Object} DataPoint
* @property {number} value - the data point's value
* @property {number} date - the data point's timestamp
* @property {string} valueString - the data point's value, expressed as a string
* @property {string} dateString - the data point's timestamp, expressed as a string
* @property {string} comment - the comment associated with the data point, or <code>null</code> if no comment exists
*/
/**
* Function for handling data point events. Note that the object passed to the function may be <code>null</code>.
*
* @callback dataPointListenerFunction
* @param {DataPoint|null} dataPoint - the {@link DataPoint}
*/
/**
* Adds the given function as a data point listener. Does nothing if the given <code>listener</code> is not a
* function.
*
* @param {dataPointListenerFunction} listener - function for handling a <code>DataPoint</code> event.
*/
this.addDataPointListener = function(listener) {
if (typeof listener === 'function') {
wrappedPlot.addDataPointListener(listener);
}
};
/**
* Removes the given data point listener. Does nothing if the given <code>listener</code> is not a function.
*
* @param {dataPointListenerFunction} listener - function for handling a <code>DataPoint</code> event.
*/
this.removeDataPointListener = function(listener) {
if (typeof listener === 'function') {
wrappedPlot.removeDataPointListener(listener);
}
};
/**
* Returns the plot's current style.
*
* @returns {Object} the style
*/
this.getStyle = function() {
return wrappedPlot.getStyle();
};
/**
* Sets the plot's style.
*
* @param {Object} the style
*/
this.setStyle = function(style) {
return wrappedPlot.setStyle(style);
};
/**
* Returns the closest <code>DataPoint</code> in this plot to the given <code>timeInSecs</code>, within the window
* of time <code>[timeInSecs - numSecsBefore, timeInSecs + numSecsAfter]</code>. Returns <code>null</code> if no
* point exists in the time window.
*
* @param timeInSecs - the time, in seconds, around which the window of time to look for the closest point is defined
* @param numSecsBefore - when defining the time window in which to look for the closest point, this is the number of seconds before the timeInSecs
* @param numSecsAfter - when defining the time window in which to look for the closest point, this is the number of seconds after the timeInSecs
* @returns {DataPoint|null}
*/
this.getClosestDataPointToTimeWithinWindow = function(timeInSecs, numSecsBefore, numSecsAfter) {
return wrappedPlot.getClosestDataPointToTimeWithinWindow(timeInSecs, numSecsBefore, numSecsAfter);
};
// the "constructor"
(function() {
if (typeof style !== 'object' || style == null) {
style = JSON.parse(JSON.stringify(DEFAULT_STYLE));
}
wrappedPlot = new DataSeriesPlot(datasource,
dateAxis.getWrappedAxis(),
yAxis.getWrappedAxis(),
{
"style" : style,
"localDisplay" : !!isLocalTime
});
})();
};
/**
* Wrapper class to make it easier to work with a plot container.
*
* @class
* @constructor
* @param {string} elementId - the DOM element ID for the container div holding this plot container
* @param {org.bodytrack.grapher.DateAxis} dateAxis - the date axis
*/
org.bodytrack.grapher.PlotContainer = function(elementId, dateAxis) {
var self = this;
var wrappedPlotContainer = null;
var yAxesAndPlotCount = {};
var plotsAndYAxes = {};
/**
* Returns the DOM element ID for the container div holding this plot container.
*
* @returns {string}
*/
this.getElementId = function() {
return elementId;
};
/**
* Returns the {@link org.bodytrack.grapher.YAxis YAxis} for the specified DOM element ID. Returns
* <code>null</code> if no such axis exists. If the given <code>yAxisElementId</code> is undefined or <code>null</code>,
* this method returns the first Y axis found, or <code>null</code> if none have been added.
*
* @param {string} [yAxisElementId] - the DOM element ID for the container div holding the desired Y axis
* @returns {org.bodytrack.grapher.YAxis}
*/
this.getYAxis = function(yAxisElementId) {
var yAxisAndPlotCount = getItemFromHashOrFindFirst(yAxesAndPlotCount, yAxisElementId);
if (yAxisAndPlotCount) {
return yAxisAndPlotCount.yAxis;
}
return null;
};
/**
* Returns the {@link org.bodytrack.grapher.DataSeriesPlot YAxis} with the specified <code>plotId</code>. Returns
* <code>null</code> if no such plot exists. If the given <code>plotId</code> is undefined or <code>null</code>,
* this method returns the first plot found, or <code>null</code> if none have been added.
*
* @param {string|number} [plotId] - A identifier for the plot, unique within the PlotContainer. Must be a number or a string.
* @returns {org.bodytrack.grapher.DataSeriesPlot}
*/
this.getPlot = function(plotId) {
var plotAndYAxis = getItemFromHashOrFindFirst(plotsAndYAxes, plotId);
if (plotAndYAxis) {
return plotAndYAxis.plot;
}
return null;
};
/**
* Adds a data series plot to the plot container. The plot will be associated with the Y axis specified by the
* given <code>yAxisElementId</code> (the Y axis may be shared with other plots, if you wish).
*
* @param {string|number} plotId - A identifier for this plot, unique within the PlotContainer. Must be a number or a string.
* @param {datasourceFunction} datasource - function with signature <code>function(level, offset, successCallback)</code> resposible for returning tile JSON for the given <code>level</code> and <code>offset</code>
* @param {string} yAxisElementId - the DOM element ID for the container div holding this plot's Y axis
* @param {number} [minValue=0] - the minimum initial value for the Y axis (if the Y axis is created for this plot). Defaults to 0 if undefined, <code>null</code>, or non-numeric.
* @param {number} [maxValue=100] - the maximum initial value for the Y axis (if the Y axis is created for this plot). Defaults to 100 if undefined, <code>null</code>, or non-numeric.
* @param {Object} [style] - the style object. A default style is used if undefined, null, or not an object.
* @param {boolean} [isLocalTime=false] - whether the plot's data uses local time. Defaults to false (UTC).
* @param {yAxisRangePaddingStrategyFunction} [yAxisRangePaddingStrategyFunction] - Y axis range padding strategy function. Defaults to the default padding strategy.
*/
this.addDataSeriesPlot = function(plotId, datasource, yAxisElementId, minValue, maxValue, style, isLocalTime, yAxisRangePaddingStrategyFunction) {
// validation
if (!isNumberOrString(plotId)) {
throw new Error("The plotId must be a number or a string.")
}
if (plotId in plotsAndYAxes) {
throw new Error("The plotId must be unique to the PlotContainer.")
}
if (typeof datasource !== 'function') {
throw new Error("The datasource must be a function.");
}
if (!isNumberOrString(yAxisElementId)) {
throw new Error("The yAxisElementId must be a number or a string.")
}
if (typeof minValue === 'undefined' || minValue == null) {
minValue = 0;
}
else if (!isNumeric(minValue)) {
throw new Error("The minValue must be a number.")
}
if (typeof maxValue === 'undefined' || maxValue == null) {
maxValue = 100;
}
else if (!isNumeric(maxValue)) {
throw new Error("The maxValue must be a number.")
}
// create the Y axis, if necessary
var yAxis = null;
if (yAxisElementId in yAxesAndPlotCount) {
// this Y axis is already used by another plot, so just get the axis and increment its plot count
yAxis = yAxesAndPlotCount[yAxisElementId].yAxis;
yAxesAndPlotCount[yAxisElementId].plotCount++;
}
else {
// this is a new Y axis, so create it and initialize its plot count to 1
yAxis = new org.bodytrack.grapher.YAxis(yAxisElementId, // DOM element ID
minValue, // initial min value for the y axis
maxValue, // initial max value for the y axis
false,
yAxisRangePaddingStrategyFunction);
yAxesAndPlotCount[yAxisElementId] = {
yAxis : yAxis,
plotCount : 1
};
}
// create the plot
var plot = new org.bodytrack.grapher.DataSeriesPlot(plotId, datasource, dateAxis, yAxis, style, isLocalTime);
plotsAndYAxes[plotId] = {
plot : plot,
yAxisElementId : yAxisElementId
};
// finally, add the plot to the plot container
wrappedPlotContainer.addPlot(plot.getWrappedPlot());
};
/**
* Removed the plot with the given <code>plotId</code> from this PlotContainer.
*
* @param {string|number} plotId - A identifier for the plot to remove, unique within the PlotContainer. Must be a number or a string.
*/
this.removePlot = function(plotId) {
if (plotId in plotsAndYAxes) {
var plot = plotsAndYAxes[plotId].plot.getWrappedPlot();
var yAxisElementId = plotsAndYAxes[plotId].yAxisElementId;
// remove the plot from the PlotContainer
wrappedPlotContainer.removePlot(plot);
// remove the plot from our collection
delete plotsAndYAxes[plotId];
// decrement the number of plots using this Y axis
yAxesAndPlotCount[yAxisElementId].plotCount--;
// see whether this Y axis is used by any other plots. If not, remove it.
if (yAxesAndPlotCount[yAxisElementId].plotCount <= 0) {
delete yAxesAndPlotCount[yAxisElementId];
// TODO: figure out a better way to remove the contents of the y axis
$("#" + yAxisElementId).find("canvas").remove();
}
}
};
/**
* Removes all plots from this PlotContainer.
*/
this.removeAllPlots = function() {
Object.keys(plotsAndYAxes).forEach(function(plotId) {
self.removePlot(plotId);
});
};
/**
* Sets the cursor for each contained plot to the color described by the given <code>colorDescriptor</code>, or to
* black if the given <code>colorDescriptor</code> is undefined, <code>null</code>, or invalid. The color
* descriptor can be any valid CSS color descriptor such as a word ("green", "blue", etc.), a hex color
* (e.g. "#ff0000"), or an RGB color (e.g. "rgb(255,0,0)" or "rgba(0,255,0,0.5)").
*
* @param {string} colorDescriptor - a string description of the desired color.
*/
this.setCursorColor = function(colorDescriptor) {
// iterate over every plot and set the cursor color in each
Object.keys(plotsAndYAxes).forEach(function(plotId) {
plotsAndYAxes[plotId].plot.setCursorColor(colorDescriptor);
});
};
/**
* Sets the width of the PlotContainer.
*
* @param {int} width - the new width
*/
this.setWidth = function(width) {
var element = $("#" + elementId);
element.width(width);
wrappedPlotContainer.setSize(width, element.height(), SequenceNumber.getNext());
};
/**
* Sets the height of the PlotContainer and all of its Y axes to the given height.
*
* @param {int} height - the new height
*/
this.setHeight = function(height) {
var element = $("#" + elementId);
element.height(height);
wrappedPlotContainer.setSize(element.width(), height, SequenceNumber.getNext());
// update the height of the Y axes
Object.keys(yAxesAndPlotCount).forEach(function(yAxisElementId) {
var yAxis = yAxesAndPlotCount[yAxisElementId].yAxis;
yAxis.setHeight(height);
});
};
/**
* Sets whether autoscaling and autoscale padding are enabled, if supported by the underlying grapher; otherwise does nothing.
*
* @param {boolean} isEnabled - whether autoscale is enabled.
* @param {boolean} [isPaddingEnabled] - whether padding of the autoscaled Y axis is enabled; ignored if
* <code>isEnabled</code> is <code>false</code>. Defaults to <code>false</code> if <code>undefined</code> or <code>null</code>.
*/
this.setAutoScaleEnabled = function(isEnabled, isPaddingEnabled) {
if (typeof wrappedPlotContainer.setAutoScaleEnabled === 'function') {
wrappedPlotContainer.setAutoScaleEnabled(!!isEnabled, !!isPaddingEnabled);
}
else {
console.log("WARN: the underlying grapher does not support autoscaling.");
}
};
// the "constructor"
(function() {
// The CREATE Lab grapher expects the date axis to be passed in as the 4th element. The BodyTrack grapher only
// expects 3 args, so passing the date axis won't hurt anything
wrappedPlotContainer = new PlotContainer(elementId, false, [], dateAxis.getWrappedAxis());
// set the width to be the same width as its DateAxis
self.setWidth(dateAxis.getWidth());
})();
};
/**
* Creates a <code>PlotManager</code> associated with date axis specified by the given
* <code>dateAxisElementId</code>. If <code>minTimeSecs</code> and <code>maxTimeSecs</code> are not specified, the
* visible time range defaults to the past 24 hours.
*
* @class
* @constructor
* @param {string} dateAxisElementId - the DOM element ID for the container div into which the date axis should be added
* @param {number} [minTimeSecs=24 hours ago] - a double representing the time in Unix time seconds of the start of the visible time range
* @param {number} [maxTimeSecs=now] - a double representing the time in Unix time seconds of the end of the visible time range
*/
org.bodytrack.grapher.PlotManager = function(dateAxisElementId, minTimeSecs, maxTimeSecs) {
var self = this;
var dateAxis = null;
var plotContainers = {};
var isWindowWidthResizeListeningEnabled = false;
var widthCalculator = null;
/**
* Returns the <code>DateAxis</code> object representing the date axis.
*
* @returns {org.bodytrack.grapher.DateAxis} the DateAxis object
*/
this.getDateAxis = function() {
return dateAxis;
};
/**
* Returns the {@link org.bodytrack.grapher.YAxis YAxis} for the specified DOM element ID. Returns
* <code>null</code> if no such axis exists. If the given <code>yAxisElementId</code> is undefined or
* <code>null</code>, this method returns the first Y axis found in the first PlotContainer found, or
* <code>null</code> if no Y axes have been added to any PlotContainer.
*
* @param {string} [yAxisElementId] - the DOM element ID for the container div holding the desired Y axis
* @returns {org.bodytrack.grapher.YAxis}
*/
this.getYAxis = function(yAxisElementId) {
// iterate over the PlotContainers this way instead of using self.forEachPlotContainer() so that we can return
// as soon as we find a matching Y axis.
var plotContainerElementIds = Object.keys(plotContainers);
for (var i = 0; i < plotContainerElementIds.length; i++) {
var plotContainerElementId = plotContainerElementIds[i];
var plotContainer = plotContainers[plotContainerElementId];
var yAxis = plotContainer.getYAxis(yAxisElementId);
if (yAxis != null) {
return yAxis;
}
}
return null;
};
/**
* Returns the first plot found with the given <code>plotId</code>. Note that since the <code>plotId</code> need
* only be unique within its {@link org.bodytrack.grapher.PlotContainer PlotContainer}, it is possible to have
* more than one plot with the same <code>plotId</code> within a PlotManager. Thus, this is merely a convenience
* method which iterates over all PlotContainers looking for the specified plot and returns the first one found.
* If no matching plot is found, this method returns null. If the given <code>plotId</code> is undefined or
* <code>null</code>, this method returns the first plot found in the first PlotContainer found, or
* <code>null</code> if no plots have been added to any PlotContainer.
*
* @param {string|number} [plotId] - A identifier for the plot, unique within its {@link org.bodytrack.grapher.PlotContainer PlotContainer}. Must be a number or a string.
* @returns {org.bodytrack.grapher.DataSeriesPlot|null}
*/
this.getPlot = function(plotId) {
// iterate over the PlotContainers this way instead of using self.forEachPlotContainer() so that we can return
// as soon as we find a matching plot.
var plotContainerElementIds = Object.keys(plotContainers);
for (var i = 0; i < plotContainerElementIds.length; i++) {
var plotContainerElementId = plotContainerElementIds[i];
var plotContainer = plotContainers[plotContainerElementId];
var plot = plotContainer.getPlot(plotId);
if (plot != null) {
return plot;
}
}
return null;
};
/**
* Returns the {@link org.bodytrack.grapher.PlotContainer PlotContainer} for the specified DOM element ID. Returns
* <code>null</code> if no such plot container exists. If the given <code>plotContainerElementId</code> is
* undefined or <code>null</code>, this method returns the first PlotContainer found, or <code>null</code> if none
* have been added.
*
* @param {string} [plotContainerElementId] - the DOM element ID for the container div holding the desired plot container.
* @returns {org.bodytrack.grapher.PlotContainer}
*/
this.getPlotContainer = function(plotContainerElementId) {
return getItemFromHashOrFindFirst(plotContainers, plotContainerElementId);
};
/**
* Adds a {@link org.bodytrack.grapher.PlotContainer PlotContainer} for the specified DOM element ID. If the
* PlotContainer has already been added, a new one is not created. Returns the PlotContainer.
*
* @param {string} plotContainerElementId - the DOM element ID for the container div holding the plot container
* @returns {org.bodytrack.grapher.PlotContainer}
*/
this.addPlotContainer = function(plotContainerElementId) {
if (!(plotContainerElementId in plotContainers)) {
plotContainers[plotContainerElementId] = new org.bodytrack.grapher.PlotContainer(plotContainerElementId, dateAxis);
}
return plotContainers[plotContainerElementId];
};
/**
* Removes the plotContainer with the given <code>plotContainerId</code> from this PlotManager.
*
* @param {string|number} plotContainerElementId - A identifier for the plotContainer to remove, unique within the PlotManager. Must be a number or a string.
*/
this.removePlotContainer = function(plotContainerElementId) {
plotContainers[plotContainerElementId].removeAllPlots();
delete plotContainers[plotContainerElementId];
};
/**
* Removes all PlotContainers from PlotManger.
*/
this.removeAllPlotContainers = function() {
Object.keys(plotContainers).forEach(function(plotContainerElementId) {
self.removePlotContainer(plotContainerElementId);
});
};
/**
* Function used by a {@link org.bodytrack.grapher.PlotContainer PlotContainer} iterator, used for performing an operation on a given PlotContainer.
*
* @callback plotContainerIteratorFunction
* @param {org.bodytrack.grapher.PlotContainer} plotContainer - the {@link org.bodytrack.grapher.PlotContainer PlotContainer} object
*/
/**
* Method for iterating over each of the {@link org.bodytrack.grapher.PlotContainer PlotContainer} instances. Does
* nothing if the given <code>plotContainerIteratorFunction</code> is not a function.
*
* @param {plotContainerIteratorFunction} plotContainerIteratorFunction
*/
this.forEachPlotContainer = function(plotContainerIteratorFunction) {
if (typeof plotContainerIteratorFunction === 'function') {
Object.keys(plotContainers).forEach(function(elementId) {
plotContainerIteratorFunction(plotContainers[elementId]);
});
}
};
/**
* Helper method for adding a plot, shorthand for
* <code>plotManager.addPlotContainer('plot_container').addDataSeriesPlot(...)</code>.
*
* @param {string|number} plotId - A identifier for this plot, unique within the {@link org.bodytrack.grapher.PlotContainer PlotContainer}. Must be a number or a string.
* @param {datasourceFunction} datasource - function with signature <code>function(level, offset, successCallback)</code> resposible for returning tile JSON for the given <code>level</code> and <code>offset</code>
* @param {string} plotContainerElementId - the DOM element ID for the container div into which this plot should be added
* @param {string} yAxisElementId - the DOM element ID for the container div holding this plot's Y axis
* @param {number} [minValue=0] - the minimum initial value for the Y axis (if the Y axis is created for this plot). Defaults to 0 if undefined, <code>null</code>, or non-numeric.
* @param {number} [maxValue=100] - the maximum initial value for the Y axis (if the Y axis is created for this plot). Defaults to 100 if undefined, <code>null</code>, or non-numeric.
* @param {Object} [style] - the style object. A default style is used if undefined, null, or not an object.
* @param {boolean} [isLocalTime=false] - whether the plot's data uses local time. Defaults to false (UTC).
* @param {yAxisRangePaddingStrategyFunction} [yAxisRangePaddingStrategyFunction] - Y axis range padding strategy function. Defaults to the default padding strategy.
*
* @see org.bodytrack.grapher.PlotContainer#addDataSeriesPlot
*/
this.addDataSeriesPlot = function(plotId, datasource, plotContainerElementId, yAxisElementId, minValue, maxValue, style, isLocalTime, yAxisRangePaddingStrategyFunction) {
self.addPlotContainer(plotContainerElementId).addDataSeriesPlot(plotId,
datasource,
yAxisElementId,
minValue,
maxValue,
style,
isLocalTime,
yAxisRangePaddingStrategyFunction);
};
/**
* Helper function which calculates and returns the desired width of the date axis and all plot containers managed
* by this {@link org.bodytrack.grapher.PlotManager PlotManager}.
*
* @callback widthCalculatorFunction
* @returns {int} the desired width of the date axis
*/
/**
* Set whether the PlotManager should auto-resize the width of the date axis and all plot containers upon resize
* of the browser window. Although the <code>widthCalculatorFunction</code> function is optional, it must be
* provided at least once (either here or via {@link #setWidthCalculator}) in order for auto-resizing to do
* anything. After setting it, you are then free to call this function with only the first argument to toggle
* auto-resizing. If the second argument is undefined <code>null</code>, or not a function, the stored function
* will not be changed.
*
* @param {boolean} willAutoResizeWidth - whether the PlotManager should auto resize the width of the date axis
* and all plot containers upon resize of the browser window.
* @param {widthCalculatorFunction} [widthCalculatorFunction] - function which calculates and returns the desired width of the date axis
*/
this.setWillAutoResizeWidth = function(willAutoResizeWidth, widthCalculatorFunction) {
isWindowWidthResizeListeningEnabled = willAutoResizeWidth;
if (typeof widthCalculatorFunction === 'function') {
widthCalculator = widthCalculatorFunction;
}
if (isWindowWidthResizeListeningEnabled) {
updateWidth();
}
};
/**
* Sets the width calculator if the given <code>widthCalculatorFunction</code> is a function. Otherwise, sets
* the calculator to <code>null</code>.
*
* @param widthCalculatorFunction
*/
this.setWidthCalculator = function(widthCalculatorFunction) {
if (typeof widthCalculatorFunction === 'function') {
widthCalculator = widthCalculatorFunction;
}
else {
widthCalculator = null;
}
};
/**
* Sets the width of the date axis and all plot containers to the given width.
*
* @param {int} newDesiredWidth - the new width
*/
this.setWidth = function(newDesiredWidth) {
dateAxis.setWidth(newDesiredWidth);
// update the width of the PlotContainers
self.forEachPlotContainer(function(plotContainer) {
plotContainer.setWidth(newDesiredWidth);
});
};
var updateWidth = function() {
if (isWindowWidthResizeListeningEnabled && widthCalculator != null) {
self.setWidth(widthCalculator());
}
};
/**
* Sets the cursor to the color described by the given <code>colorDescriptor</code>, or to black if the given
* <code>colorDescriptor</code> is undefined, <code>null</code>, or invalid. The color descriptor can be any valid
* CSS color descriptor such as a word ("green", "blue", etc.), a hex color (e.g. "#ff0000"), or an RGB color
* (e.g. "rgb(255,0,0)" or "rgba(0,255,0,0.5)").
*
* @param {string} colorDescriptor - a string description of the desired color.
*/
this.setCursorColor = function(colorDescriptor) {
// first set the cursor color in the date axis
dateAxis.setCursorColor(colorDescriptor);
// now iterate over every plot container and set the cursor color in each
self.forEachPlotContainer(function(plotContainer) {
plotContainer.setCursorColor(colorDescriptor);
});
};
// the "constructor"
(function() {
dateAxis = new org.bodytrack.grapher.DateAxis(dateAxisElementId);
// if the user didn't specify the initial visible time range, default to showing the past 24 hours
if (!isNumeric(minTimeSecs) || !isNumeric(maxTimeSecs)) {
// default the date axis to the last 24 hours
maxTimeSecs = Date.now() / 1000;
minTimeSecs = maxTimeSecs - (24 * 60 * 60);
}
// set the initial time range for the date axis
dateAxis.setRange(minTimeSecs, maxTimeSecs);
// set up window resize listener
$(window).resize(updateWidth);
})();
};
})();