diff --git a/platform/features/plot/src/elements/PlotPanZoomStack.js b/platform/features/plot/src/elements/PlotPanZoomStack.js index d31d53c128..1afb903056 100644 --- a/platform/features/plot/src/elements/PlotPanZoomStack.js +++ b/platform/features/plot/src/elements/PlotPanZoomStack.js @@ -83,7 +83,7 @@ define( * that some pan-zoom state is always available.) */ PlotPanZoomStack.prototype.popPanZoom = function popPanZoom() { - if (stack.length > 1) { + if (this.stack.length > 1) { this.stack.pop(); } }; diff --git a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js index ccb3ae3579..3746958ecb 100644 --- a/platform/features/plot/src/elements/PlotPanZoomStackGroup.js +++ b/platform/features/plot/src/elements/PlotPanZoomStackGroup.js @@ -38,15 +38,13 @@ define( * group */ function PlotPanZoomStackGroup(count) { - var stacks = [], - decoratedStacks = [], - i; + var self = this; // Push a pan-zoom state; the index argument identifies // which stack originated the request (all other stacks // will ignore the range part of the change.) function pushPanZoom(origin, dimensions, index) { - stacks.forEach(function (stack, i) { + self.stacks.forEach(function (stack, i) { if (i === index) { // Do a normal push for the specified stack stack.pushPanZoom(origin, dimensions); @@ -61,26 +59,6 @@ define( }); } - // Pop one pan-zoom state from all stacks - function popPanZoom() { - stacks.forEach(function (stack) { - stack.popPanZoom(); - }); - } - - // Set the base pan-zoom state for all stacks - function setBasePanZoom(origin, dimensions) { - stacks.forEach(function (stack) { - stack.setBasePanZoom(origin, dimensions); - }); - } - - // Clear the pan-zoom state of all stacks - function clearPanZoom() { - stacks.forEach(function (stack) { - stack.clearPanZoom(); - }); - } // Decorate a pan-zoom stack; returns an object with // the same interface, but whose stack-mutation methods @@ -92,88 +70,101 @@ define( result.pushPanZoom = function (origin, dimensions) { pushPanZoom(origin, dimensions, index); }; - result.setBasePanZoom = setBasePanZoom; - result.popPanZoom = popPanZoom; - result.clearPanZoom = clearPanZoom; + result.setBasePanZoom = function () { + self.setBasePanZoom.apply(self, arguments); + }; + result.popPanZoom = function () { + self.popPanZoom.apply(self, arguments); + }; + result.clearPanZoom = function () { + self.clearPanZoom.apply(self, arguments); + }; return result; } // Create the stacks in this group ... - while (stacks.length < count) { - stacks.push(new PlotPanZoomStack([], [])); + this.stacks = []; + while (this.stacks.length < count) { + this.stacks.push(new PlotPanZoomStack([], [])); } // ... and their decorated-to-synchronize versions. - decoratedStacks = stacks.map(decorateStack); - - return { - /** - * Pop a pan-zoom state from all stacks in the group. - * If called when there is only one pan-zoom state on each - * stack, this acts as a no-op (that is, the lowest - * pan-zoom state on the stack cannot be popped, to ensure - * that some pan-zoom state is always available.) - * @memberof platform/features/plot.PlotPanZoomStackGroup# - */ - popPanZoom: popPanZoom, - - /** - * Set the base pan-zoom state for all stacks in this group. - * This changes the state at the bottom of each stack. - * This allows the "unzoomed" state of plots to be updated - * (e.g. as new data comes in) without - * interfering with the user's chosen pan/zoom states. - * @param {number[]} origin the base origin - * @param {number[]} dimensions the base dimensions - * @memberof platform/features/plot.PlotPanZoomStackGroup# - */ - setBasePanZoom: setBasePanZoom, - - /** - * Clear all pan-zoom stacks in this group down to - * their bottom element; in effect, pop all elements - * but the last, e.g. to remove any temporary user - * modifications to pan-zoom state. - * @memberof platform/features/plot.PlotPanZoomStackGroup# - */ - clearPanZoom: clearPanZoom, - /** - * Get the current stack depth; that is, the number - * of items on each stack in the group. - * A depth of one means that no - * panning or zooming relative to the base value has - * been applied. - * @returns {number} the depth of the stacks in this group - * @memberof platform/features/plot.PlotPanZoomStackGroup# - */ - getDepth: function () { - // All stacks are kept in sync, so look up depth - // from the first one. - return stacks.length > 0 ? - stacks[0].getDepth() : 0; - }, - /** - * Get a specific pan-zoom stack in this group. - * Stacks are specified by index; this index must be less - * than the count provided at construction time, and must - * not be less than zero. - * The stack returned by this function will be synchronized - * to other stacks in this group; that is, mutating that - * stack directly will result in other stacks in this group - * undergoing similar updates to ensure that domain bounds - * remain the same. - * @param {number} index the index of the stack to get - * @returns {PlotPanZoomStack} the pan-zoom stack in the - * group identified by that index - * @memberof platform/features/plot.PlotPanZoomStackGroup# - */ - getPanZoomStack: function (index) { - return decoratedStacks[index]; - } - }; - + this.decoratedStacks = this.stacks.map(decorateStack); } + /** + * Pop a pan-zoom state from all stacks in the group. + * If called when there is only one pan-zoom state on each + * stack, this acts as a no-op (that is, the lowest + * pan-zoom state on the stack cannot be popped, to ensure + * that some pan-zoom state is always available.) + */ + PlotPanZoomStackGroup.prototype.popPanZoom = function () { + this.stacks.forEach(function (stack) { + stack.popPanZoom(); + }); + }; + + /** + * Set the base pan-zoom state for all stacks in this group. + * This changes the state at the bottom of each stack. + * This allows the "unzoomed" state of plots to be updated + * (e.g. as new data comes in) without + * interfering with the user's chosen pan/zoom states. + * @param {number[]} origin the base origin + * @param {number[]} dimensions the base dimensions + */ + PlotPanZoomStackGroup.prototype.setBasePanZoom = function (origin, dimensions) { + this.stacks.forEach(function (stack) { + stack.setBasePanZoom(origin, dimensions); + }); + }; + + /** + * Clear all pan-zoom stacks in this group down to + * their bottom element; in effect, pop all elements + * but the last, e.g. to remove any temporary user + * modifications to pan-zoom state. + */ + PlotPanZoomStackGroup.prototype.clearPanZoom = function () { + this.stacks.forEach(function (stack) { + stack.clearPanZoom(); + }); + }; + + /** + * Get the current stack depth; that is, the number + * of items on each stack in the group. + * A depth of one means that no + * panning or zooming relative to the base value has + * been applied. + * @returns {number} the depth of the stacks in this group + */ + PlotPanZoomStackGroup.prototype.getDepth = function () { + // All stacks are kept in sync, so look up depth + // from the first one. + return this.stacks.length > 0 ? + this.stacks[0].getDepth() : 0; + }; + + /** + * Get a specific pan-zoom stack in this group. + * Stacks are specified by index; this index must be less + * than the count provided at construction time, and must + * not be less than zero. + * The stack returned by this function will be synchronized + * to other stacks in this group; that is, mutating that + * stack directly will result in other stacks in this group + * undergoing similar updates to ensure that domain bounds + * remain the same. + * @param {number} index the index of the stack to get + * @returns {PlotPanZoomStack} the pan-zoom stack in the + * group identified by that index + */ + PlotPanZoomStackGroup.prototype.getPanZoomStack = function (index) { + return this.decoratedStacks[index]; + }; + return PlotPanZoomStackGroup; } ); diff --git a/platform/features/plot/src/elements/PlotPosition.js b/platform/features/plot/src/elements/PlotPosition.js index 30220f56f6..15444e68d5 100644 --- a/platform/features/plot/src/elements/PlotPosition.js +++ b/platform/features/plot/src/elements/PlotPosition.js @@ -48,8 +48,7 @@ define( function PlotPosition(x, y, width, height, panZoomStack) { var panZoom = panZoomStack.getPanZoom(), origin = panZoom.origin, - dimensions = panZoom.dimensions, - position; + dimensions = panZoom.dimensions; function convert(v, i) { return v * dimensions[i] + origin[i]; @@ -57,45 +56,42 @@ define( if (!dimensions || !origin) { // We need both dimensions and origin to compute a position - position = []; + this.position = []; } else { // Convert from pixel to domain-range space. // Note that range is reversed from the y-axis in pixel space //(positive range points up, positive pixel-y points down) - position = [ x / width, (height - y) / height ].map(convert); + this.position = + [ x / width, (height - y) / height ].map(convert); } - - return { - /** - * Get the domain value corresponding to this pixel position. - * @returns {number} the domain value - * @memberof platform/features/plot.PlotPosition# - */ - getDomain: function () { - return position[0]; - }, - /** - * Get the range value corresponding to this pixel position. - * @returns {number} the range value - * @memberof platform/features/plot.PlotPosition# - */ - getRange: function () { - return position[1]; - }, - /** - * Get the domain and values corresponding to this - * pixel position. - * @returns {number[]} an array containing the domain and - * the range value, in that order - * @memberof platform/features/plot.PlotPosition# - */ - getPosition: function () { - return position; - } - }; - } + /** + * Get the domain value corresponding to this pixel position. + * @returns {number} the domain value + */ + PlotPosition.prototype.getDomain = function () { + return this.position[0]; + }; + + /** + * Get the range value corresponding to this pixel position. + * @returns {number} the range value + */ + PlotPosition.prototype.getRange =function () { + return this.position[1]; + }; + + /** + * Get the domain and values corresponding to this + * pixel position. + * @returns {number[]} an array containing the domain and + * the range value, in that order + */ + PlotPosition.prototype.getPosition = function () { + return this.position; + }; + return PlotPosition; } ); diff --git a/platform/features/plot/src/elements/PlotPreparer.js b/platform/features/plot/src/elements/PlotPreparer.js index 87049c9198..a1ca0cc5b5 100644 --- a/platform/features/plot/src/elements/PlotPreparer.js +++ b/platform/features/plot/src/elements/PlotPreparer.js @@ -49,8 +49,7 @@ define( min = [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], x, y, - domainOffset = Number.POSITIVE_INFINITY, - buffers; + domainOffset = Number.POSITIVE_INFINITY; // Remove any undefined data sets datas = (datas || []).filter(identity); @@ -85,65 +84,69 @@ define( } // Convert to Float32Array - buffers = vertices.map(function (v) { return new Float32Array(v); }); + this.buffers = vertices.map(function (v) { + return new Float32Array(v); + }); - return { - /** - * Get the dimensions which bound all data in the provided - * data sets. This is given as a two-element array where the - * first element is domain, and second is range. - * @returns {number[]} the dimensions which bound this data set - * @memberof platform/features/plot.PlotPreparer# - */ - getDimensions: function () { - return [max[0] - min[0], max[1] - min[1]]; - }, - /** - * Get the origin of this data set's boundary. - * This is given as a two-element array where the - * first element is domain, and second is range. - * The domain value here is not adjusted by the domain offset. - * @returns {number[]} the origin of this data set's boundary - * @memberof platform/features/plot.PlotPreparer# - */ - getOrigin: function () { - return min; - }, - /** - * Get the domain offset; this offset will have been subtracted - * from all domain values in all buffers returned by this - * preparer, in order to minimize loss-of-precision due to - * conversion to the 32-bit float format needed by WebGL. - * @returns {number} the domain offset - * @memberof platform/features/plot.PlotPreparer# - */ - getDomainOffset: function () { - return domainOffset; - }, - /** - * Get all renderable buffers for this data set. This will - * be returned as an array which can be correlated back to - * the provided telemetry data objects (from the constructor - * call) by index. - * - * Internally, these are flattened; each buffer contains a - * sequence of alternating domain and range values. - * - * All domain values in all buffers will have been adjusted - * from their original values by subtraction of the domain - * offset; this minimizes loss-of-precision resulting from - * the conversion to 32-bit floats, which may otherwise - * cause aliasing artifacts (particularly for timestamps) - * - * @returns {Float32Array[]} the buffers for these traces - * @memberof platform/features/plot.PlotPreparer# - */ - getBuffers: function () { - return buffers; - } - }; + this.min = min; + this.max = max; + this.domainOffset = domainOffset; } + /** + * Get the dimensions which bound all data in the provided + * data sets. This is given as a two-element array where the + * first element is domain, and second is range. + * @returns {number[]} the dimensions which bound this data set + */ + PlotPreparer.prototype.getDimensions = function () { + var max = this.max, min = this.min; + return [max[0] - min[0], max[1] - min[1]]; + }; + + /** + * Get the origin of this data set's boundary. + * This is given as a two-element array where the + * first element is domain, and second is range. + * The domain value here is not adjusted by the domain offset. + * @returns {number[]} the origin of this data set's boundary + */ + PlotPreparer.prototype.getOrigin = function () { + return this.min; + }; + + /** + * Get the domain offset; this offset will have been subtracted + * from all domain values in all buffers returned by this + * preparer, in order to minimize loss-of-precision due to + * conversion to the 32-bit float format needed by WebGL. + * @returns {number} the domain offset + */ + PlotPreparer.prototype.getDomainOffset = function () { + return this.domainOffset; + }; + + /** + * Get all renderable buffers for this data set. This will + * be returned as an array which can be correlated back to + * the provided telemetry data objects (from the constructor + * call) by index. + * + * Internally, these are flattened; each buffer contains a + * sequence of alternating domain and range values. + * + * All domain values in all buffers will have been adjusted + * from their original values by subtraction of the domain + * offset; this minimizes loss-of-precision resulting from + * the conversion to 32-bit floats, which may otherwise + * cause aliasing artifacts (particularly for timestamps) + * + * @returns {Float32Array[]} the buffers for these traces + */ + PlotPreparer.prototype.getBuffers = function () { + return this.buffers; + }; + return PlotPreparer; } diff --git a/platform/features/plot/src/elements/PlotSeriesWindow.js b/platform/features/plot/src/elements/PlotSeriesWindow.js index bff0710b34..4bf880a239 100644 --- a/platform/features/plot/src/elements/PlotSeriesWindow.js +++ b/platform/features/plot/src/elements/PlotSeriesWindow.js @@ -30,41 +30,53 @@ define( * insertion into a plot line. * @constructor * @memberof platform/features/plot + * @implements {TelemetrySeries} */ function PlotSeriesWindow(series, domain, range, start, end) { - return { - getPointCount: function () { - return end - start; - }, - getDomainValue: function (index) { - return series.getDomainValue(index + start, domain); - }, - getRangeValue: function (index) { - return series.getRangeValue(index + start, range); - }, - split: function () { - var mid = Math.floor((end + start) / 2); - return ((end - start) > 1) ? - [ - new PlotSeriesWindow( - series, - domain, - range, - start, - mid - ), - new PlotSeriesWindow( - series, - domain, - range, - mid, - end - ) - ] : []; - } - }; + this.series = series; + this.domain = domain; + this.range = range; + this.start = start; + this.end = end; } + PlotSeriesWindow.prototype.getPointCount = function () { + return this.end - this.start; + }; + + PlotSeriesWindow.prototype.getDomainValue = function (index) { + return this.series.getDomainValue(index + this.start, this.domain); + }; + + PlotSeriesWindow.prototype.getRangeValue = function (index) { + return this.series.getRangeValue(index + this.start, this.range); + }; + + /** + * Split this series into two series of equal (or nearly-equal) size. + * @returns {PlotSeriesWindow[]} two series + */ + PlotSeriesWindow.prototype.split = function () { + var mid = Math.floor((this.end + this.start) / 2); + return ((this.end - this.start) > 1) ? + [ + new PlotSeriesWindow( + this.series, + this.domain, + this.range, + this.start, + mid + ), + new PlotSeriesWindow( + this.series, + this.domain, + this.range, + mid, + this.end + ) + ] : []; + }; + return PlotSeriesWindow; } ); diff --git a/platform/features/plot/src/elements/PlotTickGenerator.js b/platform/features/plot/src/elements/PlotTickGenerator.js index 024338eb4d..af18050955 100644 --- a/platform/features/plot/src/elements/PlotTickGenerator.js +++ b/platform/features/plot/src/elements/PlotTickGenerator.js @@ -39,60 +39,56 @@ define( * domain and range values. */ function PlotTickGenerator(panZoomStack, formatter) { + this.panZoomStack = panZoomStack; + this.formatter = formatter; + } - // Generate ticks; interpolate from start up to - // start + span in count steps, using the provided - // formatter to represent each value. - function generateTicks(start, span, count, format) { - var step = span / (count - 1), - result = [], - i; + // Generate ticks; interpolate from start up to + // start + span in count steps, using the provided + // formatter to represent each value. + PlotTickGenerator.prototype.generateTicks = function (start, span, count, format) { + var step = span / (count - 1), + result = [], + i; - for (i = 0; i < count; i += 1) { - result.push({ - label: format(i * step + start) - }); - } - - return result; + for (i = 0; i < count; i += 1) { + result.push({ + label: format(i * step + start) + }); } + return result; + }; - return { - /** - * Generate tick marks for the domain axis. - * @param {number} count the number of ticks - * @returns {string[]} labels for those ticks - * @memberof platform/features/plot.PlotTickGenerator# - */ - generateDomainTicks: function (count) { - var panZoom = panZoomStack.getPanZoom(); - return generateTicks( - panZoom.origin[0], - panZoom.dimensions[0], - count, - formatter.formatDomainValue - ); - }, + /** + * Generate tick marks for the domain axis. + * @param {number} count the number of ticks + * @returns {string[]} labels for those ticks + */ + PlotTickGenerator.prototype.generateDomainTicks = function (count) { + var panZoom = this.panZoomStack.getPanZoom(); + return this.generateTicks( + panZoom.origin[0], + panZoom.dimensions[0], + count, + this.formatter.formatDomainValue + ); + }; - /** - * Generate tick marks for the range axis. - * @param {number} count the number of ticks - * @returns {string[]} labels for those ticks - * @memberof platform/features/plot.PlotTickGenerator# - */ - generateRangeTicks: function (count) { - var panZoom = panZoomStack.getPanZoom(); - return generateTicks( - panZoom.origin[1], - panZoom.dimensions[1], - count, - formatter.formatRangeValue - ); - } - }; - - } + /** + * Generate tick marks for the range axis. + * @param {number} count the number of ticks + * @returns {string[]} labels for those ticks + */ + PlotTickGenerator.prototype.generateRangeTicks = function (count) { + var panZoom = this.panZoomStack.getPanZoom(); + return this.generateTicks( + panZoom.origin[1], + panZoom.dimensions[1], + count, + this.formatter.formatRangeValue + ); + }; return PlotTickGenerator; } diff --git a/platform/features/plot/src/elements/PlotUpdater.js b/platform/features/plot/src/elements/PlotUpdater.js index d37112c243..851fa56096 100644 --- a/platform/features/plot/src/elements/PlotUpdater.js +++ b/platform/features/plot/src/elements/PlotUpdater.js @@ -21,10 +21,6 @@ *****************************************************************************/ /*global define,Float32Array*/ -/** - * Prepares data to be rendered in a GL Plot. Handles - * the conversion from data API to displayable buffers. - */ define( ['./PlotLine', './PlotLineBuffer'], function (PlotLine, PlotLineBuffer) { @@ -44,302 +40,282 @@ define( * @param {TelemetryHandle} handle the handle to telemetry access * @param {string} domain the key to use when looking up domain values * @param {string} range the key to use when looking up range values - * @param {number} maxDuration maximum plot duration to display + * @param {number} fixedDuration maximum plot duration to display * @param {number} maxPoints maximum number of points to display */ function PlotUpdater(handle, domain, range, fixedDuration, maxPoints) { - var ids = [], - lines = {}, - dimensions = [0, 0], - origin = [0, 0], - domainExtrema, - rangeExtrema, - buffers = {}, - bufferArray = [], - domainOffset; + this.handle = handle; + this.domain = domain; + this.range = range; + this.fixedDuration = fixedDuration; + this.maxPoints = maxPoints; - // Look up a domain object's id (for mapping, below) - function getId(domainObject) { - return domainObject.getId(); - } - - // Check if this set of ids matches the current set of ids - // (used to detect if line preparation can be skipped) - function idsMatch(nextIds) { - return ids.length === nextIds.length && - nextIds.every(function (id, index) { - return ids[index] === id; - }); - } - - // Prepare plot lines for this group of telemetry objects - function prepareLines(telemetryObjects) { - var nextIds = telemetryObjects.map(getId), - next = {}; - - // Detect if we already have everything we need prepared - if (idsMatch(nextIds)) { - // Nothing to prepare, move on - return; - } - - // Built up a set of ids. Note that we can only - // create plot lines after our domain offset has - // been determined. - if (domainOffset !== undefined) { - // Update list of ids in use - ids = nextIds; - - // Create buffers for these objects - bufferArray = ids.map(function (id) { - buffers[id] = buffers[id] || new PlotLineBuffer( - domainOffset, - INITIAL_SIZE, - maxPoints - ); - next[id] = lines[id] || new PlotLine(buffers[id]); - return buffers[id]; - }); - } - - // If there are no more lines, clear the domain offset - if (Object.keys(next).length < 1) { - domainOffset = undefined; - } - - // Update to the current set of lines - lines = next; - } - - - // Initialize the domain offset, based on these observed values - function initializeDomainOffset(values) { - domainOffset = - ((domainOffset === undefined) && (values.length > 0)) ? - (values.reduce(function (a, b) { - return (a || 0) + (b || 0); - }, 0) / values.length) : - domainOffset; - } - - // Used in the reduce step of updateExtrema - function reduceExtrema(a, b) { - return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ]; - } - - // Convert a domain/range extrema to plot dimensions - function dimensionsOf(extrema) { - return extrema[1] - extrema[0]; - } - - // Convert a domain/range extrema to a plot origin - function originOf(extrema) { - return extrema[0]; - } - - // Expand range slightly so points near edges are visible - function expandRange() { - var padding = PADDING_RATIO * dimensions[1], - top; - padding = Math.max(padding, 1.0); - top = Math.ceil(origin[1] + dimensions[1] + padding / 2); - origin[1] = Math.floor(origin[1] - padding / 2); - dimensions[1] = top - origin[1]; - } - - // Update dimensions and origin based on extrema of plots - function updateBounds() { - if (bufferArray.length > 0) { - domainExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getDomainExtrema(); - }).reduce(reduceExtrema); - - rangeExtrema = bufferArray.map(function (lineBuffer) { - return lineBuffer.getRangeExtrema(); - }).reduce(reduceExtrema); - - // Calculate best-fit dimensions - dimensions = - [dimensionsOf(domainExtrema), dimensionsOf(rangeExtrema)]; - origin = [originOf(domainExtrema), originOf(rangeExtrema)]; - - // Enforce some minimum visible area - expandRange(); - - // ...then enforce a fixed duration if needed - if (fixedDuration !== undefined) { - origin[0] = origin[0] + dimensions[0] - fixedDuration; - dimensions[0] = fixedDuration; - } - } - } - - // Enforce maximum duration on all plot lines; not that - // domain extrema must be up-to-date for this to behave correctly. - function enforceDuration() { - var cutoff; - - function enforceDurationForBuffer(plotLineBuffer) { - var index = plotLineBuffer.findInsertionIndex(cutoff); - if (index > 0) { - // Leave one point untrimmed, such that line will - // continue off left edge of visible plot area. - plotLineBuffer.trim(index - 1); - } - } - - if (fixedDuration !== undefined && - domainExtrema !== undefined && - (domainExtrema[1] - domainExtrema[0] > fixedDuration)) { - cutoff = domainExtrema[1] - fixedDuration; - bufferArray.forEach(enforceDurationForBuffer); - updateBounds(); // Extrema may have changed now - } - } - - // Add latest data for this domain object - function addPointFor(domainObject) { - var line = lines[domainObject.getId()]; - if (line) { - line.addPoint( - handle.getDomainValue(domainObject, domain), - handle.getRangeValue(domainObject, range) - ); - } - } - - // Handle new telemetry data - function update() { - var objects = handle.getTelemetryObjects(); - - // Initialize domain offset if necessary - if (domainOffset === undefined) { - initializeDomainOffset(objects.map(function (obj) { - return handle.getDomainValue(obj, domain); - }).filter(function (value) { - return typeof value === 'number'; - })); - } - - // Make sure lines are available - prepareLines(objects); - - // Add new data - objects.forEach(addPointFor); - - // Then, update extrema - updateBounds(); - } - - // Add historical data for this domain object - function setHistorical(domainObject, series) { - var count = series ? series.getPointCount() : 0, - line; - - // Nothing to do if it's an empty series - if (count < 1) { - return; - } - - // Initialize domain offset if necessary - if (domainOffset === undefined) { - initializeDomainOffset([ - series.getDomainValue(0, domain), - series.getDomainValue(count - 1, domain) - ]); - } - - // Make sure lines are available - prepareLines(handle.getTelemetryObjects()); - - // Look up the line for this domain object - line = lines[domainObject.getId()]; - - // ...and put the data into it. - if (line) { - line.addSeries(series, domain, range); - } - - // Update extrema - updateBounds(); - } + this.ids = []; + this.lines = {}; + this.buffers = {}; + this.bufferArray = []; // Use a default MAX_POINTS if none is provided - maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS; + this.maxPoints = maxPoints !== undefined ? maxPoints : MAX_POINTS; + this.dimensions = [0, 0]; + this.origin = [0, 0]; // Initially prepare state for these objects. // Note that this may be an empty array at this time, // so we also need to check during update cycles. - update(); - - return { - /** - * Get the dimensions which bound all data in the provided - * data sets. This is given as a two-element array where the - * first element is domain, and second is range. - * @returns {number[]} the dimensions which bound this data set - * @memberof platform/features/plot.PlotUpdater# - */ - getDimensions: function () { - return dimensions; - }, - /** - * Get the origin of this data set's boundary. - * This is given as a two-element array where the - * first element is domain, and second is range. - * The domain value here is not adjusted by the domain offset. - * @returns {number[]} the origin of this data set's boundary - * @memberof platform/features/plot.PlotUpdater# - */ - getOrigin: function () { - // Pad range if necessary - return origin; - }, - /** - * Get the domain offset; this offset will have been subtracted - * from all domain values in all buffers returned by this - * preparer, in order to minimize loss-of-precision due to - * conversion to the 32-bit float format needed by WebGL. - * @returns {number} the domain offset - * @memberof platform/features/plot.PlotUpdater# - */ - getDomainOffset: function () { - return domainOffset; - }, - /** - * Get all renderable buffers for this data set. This will - * be returned as an array which can be correlated back to - * the provided telemetry data objects (from the constructor - * call) by index. - * - * Internally, these are flattened; each buffer contains a - * sequence of alternating domain and range values. - * - * All domain values in all buffers will have been adjusted - * from their original values by subtraction of the domain - * offset; this minimizes loss-of-precision resulting from - * the conversion to 32-bit floats, which may otherwise - * cause aliasing artifacts (particularly for timestamps) - * - * @returns {Float32Array[]} the buffers for these traces - * @memberof platform/features/plot.PlotUpdater# - */ - getLineBuffers: function () { - return bufferArray; - }, - /** - * Update with latest data. - * @memberof platform/features/plot.PlotUpdater# - */ - update: update, - /** - * Fill in historical data. - * @memberof platform/features/plot.PlotUpdater# - */ - addHistorical: setHistorical - }; + this.update(); } + // Look up a domain object's id (for mapping, below) + function getId(domainObject) { + return domainObject.getId(); + } + + // Used in the reduce step of updateExtrema + function reduceExtrema(a, b) { + return [ Math.min(a[0], b[0]), Math.max(a[1], b[1]) ]; + } + + // Convert a domain/range extrema to plot dimensions + function dimensionsOf(extrema) { + return extrema[1] - extrema[0]; + } + + // Convert a domain/range extrema to a plot origin + function originOf(extrema) { + return extrema[0]; + } + + // Check if this set of ids matches the current set of ids + // (used to detect if line preparation can be skipped) + PlotUpdater.prototype.idsMatch = function (nextIds) { + var ids = this.ids; + return ids.length === nextIds.length && + nextIds.every(function (id, index) { + return ids[index] === id; + }); + }; + + // Prepare plot lines for this group of telemetry objects + PlotUpdater.prototype.prepareLines = function (telemetryObjects) { + var nextIds = telemetryObjects.map(getId), + next = {}, + self = this; + + // Detect if we already have everything we need prepared + if (this.idsMatch(nextIds)) { + // Nothing to prepare, move on + return; + } + + // Built up a set of ids. Note that we can only + // create plot lines after our domain offset has + // been determined. + if (this.domainOffset !== undefined) { + // Update list of ids in use + this.ids = nextIds; + + // Create buffers for these objects + this.bufferArray = this.ids.map(function (id) { + self.buffers[id] = self.buffers[id] || new PlotLineBuffer( + self.domainOffset, + INITIAL_SIZE, + self.maxPoints + ); + next[id] = + self.lines[id] || new PlotLine(self.buffers[id]); + return self.buffers[id]; + }); + } + + // If there are no more lines, clear the domain offset + if (Object.keys(next).length < 1) { + this.domainOffset = undefined; + } + + // Update to the current set of lines + this.lines = next; + }; + + // Initialize the domain offset, based on these observed values + PlotUpdater.prototype.initializeDomainOffset = function (values) { + this.domainOffset = + ((this.domainOffset === undefined) && (values.length > 0)) ? + (values.reduce(function (a, b) { + return (a || 0) + (b || 0); + }, 0) / values.length) : + this.domainOffset; + }; + + // Expand range slightly so points near edges are visible + PlotUpdater.prototype.expandRange = function () { + var padding = PADDING_RATIO * this.dimensions[1], + top; + padding = Math.max(padding, 1.0); + top = Math.ceil(this.origin[1] + this.dimensions[1] + padding / 2); + this.origin[1] = Math.floor(this.origin[1] - padding / 2); + this.dimensions[1] = top - this.origin[1]; + }; + + // Update dimensions and origin based on extrema of plots + PlotUpdater.prototype.updateBounds = function () { + var bufferArray = this.bufferArray; + if (bufferArray.length > 0) { + this.domainExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getDomainExtrema(); + }).reduce(reduceExtrema); + + this.rangeExtrema = bufferArray.map(function (lineBuffer) { + return lineBuffer.getRangeExtrema(); + }).reduce(reduceExtrema); + + // Calculate best-fit dimensions + this.dimensions = [ this.domainExtrema, this.rangeExtrema ] + .map(dimensionsOf); + this.origin = [ this.domainExtrema, this.rangeExtrema ] + .map(originOf); + + // Enforce some minimum visible area + this.expandRange(); + + // ...then enforce a fixed duration if needed + if (this.fixedDuration !== undefined) { + this.origin[0] = this.origin[0] + this.dimensions[0] - + this.fixedDuration; + this.dimensions[0] = this.fixedDuration; + } + } + }; + + // Add latest data for this domain object + PlotUpdater.prototype.addPointFor = function (domainObject) { + var line = this.lines[domainObject.getId()]; + if (line) { + line.addPoint( + this.handle.getDomainValue(domainObject, this.domain), + this.handle.getRangeValue(domainObject, this.range) + ); + } + }; + + /** + * Update with latest data. + */ + PlotUpdater.prototype.update = function update() { + var objects = this.handle.getTelemetryObjects(), + self = this; + + // Initialize domain offset if necessary + if (this.domainOffset === undefined) { + this.initializeDomainOffset(objects.map(function (obj) { + return self.handle.getDomainValue(obj, self.domain); + }).filter(function (value) { + return typeof value === 'number'; + })); + } + + // Make sure lines are available + this.prepareLines(objects); + + // Add new data + objects.forEach(function (domainObject, index) { + self.addPointFor(domainObject, index); + }); + + // Then, update extrema + this.updateBounds(); + }; + + /** + * Get the dimensions which bound all data in the provided + * data sets. This is given as a two-element array where the + * first element is domain, and second is range. + * @returns {number[]} the dimensions which bound this data set + */ + PlotUpdater.prototype.getDimensions = function () { + return this.dimensions; + }; + + /** + * Get the origin of this data set's boundary. + * This is given as a two-element array where the + * first element is domain, and second is range. + * The domain value here is not adjusted by the domain offset. + * @returns {number[]} the origin of this data set's boundary + */ + PlotUpdater.prototype.getOrigin = function () { + return this.origin; + }; + + /** + * Get the domain offset; this offset will have been subtracted + * from all domain values in all buffers returned by this + * preparer, in order to minimize loss-of-precision due to + * conversion to the 32-bit float format needed by WebGL. + * @returns {number} the domain offset + * @memberof platform/features/plot.PlotUpdater# + */ + PlotUpdater.prototype.getDomainOffset = function () { + return this.domainOffset; + }; + + /** + * Get all renderable buffers for this data set. This will + * be returned as an array which can be correlated back to + * the provided telemetry data objects (from the constructor + * call) by index. + * + * Internally, these are flattened; each buffer contains a + * sequence of alternating domain and range values. + * + * All domain values in all buffers will have been adjusted + * from their original values by subtraction of the domain + * offset; this minimizes loss-of-precision resulting from + * the conversion to 32-bit floats, which may otherwise + * cause aliasing artifacts (particularly for timestamps) + * + * @returns {Float32Array[]} the buffers for these traces + * @memberof platform/features/plot.PlotUpdater# + */ + PlotUpdater.prototype.getLineBuffers = function () { + return this.bufferArray; + }; + + /** + * Fill in historical data. + */ + PlotUpdater.prototype.addHistorical = function (domainObject, series) { + var count = series ? series.getPointCount() : 0, + line; + + // Nothing to do if it's an empty series + if (count < 1) { + return; + } + + // Initialize domain offset if necessary + if (this.domainOffset === undefined) { + this.initializeDomainOffset([ + series.getDomainValue(0, this.domain), + series.getDomainValue(count - 1, this.domain) + ]); + } + + // Make sure lines are available + this.prepareLines(this.handle.getTelemetryObjects()); + + // Look up the line for this domain object + line = this.lines[domainObject.getId()]; + + // ...and put the data into it. + if (line) { + line.addSeries(series, this.domain, this.range); + } + + // Update extrema + this.updateBounds(); + }; + return PlotUpdater; }