downsample
Version:
Provides functions for time series data downsampling for visual representation
415 lines (341 loc) • 13.8 kB
JavaScript
'use strict';
Object.defineProperty(exports, '__esModule', { value: true });
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArrayLimit(arr, i) {
if (typeof Symbol === "undefined" || !(Symbol.iterator in Object(arr))) return;
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _arrayLikeToArray(arr, len) {
if (len == null || len > arr.length) len = arr.length;
for (var i = 0, arr2 = new Array(len); i < len; i++) {
arr2[i] = arr[i];
}
return arr2;
}
function _unsupportedIterableToArray(o, minLen) {
if (!o) return;
if (typeof o === "string") return _arrayLikeToArray(o, minLen);
var n = Object.prototype.toString.call(o).slice(8, -1);
if (n === "Object" && o.constructor) n = o.constructor.name;
if (n === "Map" || n === "Set") return Array.from(o);
if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _unsupportedIterableToArray(arr, i) || _nonIterableRest();
}
var __isA = {
"PointValueExtractor<unknown>": value => typeof value === "function",
"XYDataPoint": value => value !== undefined && value !== null && __isA["X"](value["x"]) && typeof value["y"] === "number",
"X": value => typeof value === "number" || value instanceof Date
};
function calculateTriangleArea(pointA, pointB, pointC) {
return Math.abs((pointA[0] - pointC[0]) * (pointB[1] - pointA[1]) - (pointA[0] - pointB[0]) * (pointC[1] - pointA[1])) / 2;
}
function calculateAverageDataPoint() {
for (var _len = arguments.length, points = new Array(_len), _key = 0; _key < _len; _key++) {
points[_key] = arguments[_key];
}
var length = points.length;
if (!length) return undefined;
var averageX = 0;
var averageY = 0;
for (var i = 0; i < length; i++) {
averageX += points[i][0];
averageY += points[i][1];
}
return [averageX / length, averageY / length];
}
function splitIntoBuckets(data, desiredLength) {
if (data.length === 2) {
return [[data[0]], [data[1]]];
}
var first = data[0];
var center = data.slice(1, data.length - 1);
var last = data[data.length - 1]; // First and last bucket are formed by the first and the last data points
// so we only have N - 2 buckets left to fill
var bucketSize = center.length / (desiredLength - 2);
var splitData = [[first]];
for (var i = 0; i < desiredLength - 2; i++) {
var bucketStartIndex = Math.floor(i * bucketSize);
var bucketEndIndex = Math.floor((i + 1) * bucketSize);
var dataPointsInBucket = center.slice(bucketStartIndex, bucketEndIndex);
splitData.push(dataPointsInBucket);
}
splitData.push([last]);
return splitData;
}
var mapToArray = (input, callback) => {
var length = input.length;
var result = new Array(length);
for (var i = 0; i < length; i++) {
result[i] = callback(input[i], i);
}
return result;
};
var getPointValueExtractor = accessor => {
if (__isA["PointValueExtractor<unknown>"](accessor)) return accessor;
return point => point[accessor];
};
var createNormalize = (x, y) => {
var getX = getPointValueExtractor(x);
var getY = getPointValueExtractor(y);
return data => mapToArray(data, (point, index) => [getX(point, index), getY(point, index)]);
};
var createXYDataPoint = (time, value) => ({
x: time,
y: value
});
var createLegacyDataPointConfig = () => ({
x: point => {
var t = __isA["XYDataPoint"](point) ? point.x : point[0];
return t instanceof Date ? t.getTime() : t;
},
y: point => 'y' in point ? point.y : point[1],
toPoint: createXYDataPoint
});
var iterableBasedOn = (input, length) => new input.constructor(length);
function LTTBIndexesForBuckets(buckets) {
var bucketCount = buckets.length;
var bucketDataPointIndexes = [0];
var previousBucketsSize = 1;
var lastSelectedDataPoint = buckets[0][0];
for (var index = 1; index < bucketCount - 1; index++) {
var bucket = buckets[index];
var nextBucket = buckets[index + 1];
var averageDataPointFromNextBucket = calculateAverageDataPoint(...nextBucket);
if (averageDataPointFromNextBucket === undefined) continue;
var maxArea = -1;
var maxAreaIndex = -1;
for (var j = 0; j < bucket.length; j++) {
var dataPoint = bucket[j];
var area = calculateTriangleArea(lastSelectedDataPoint, dataPoint, averageDataPointFromNextBucket);
if (area > maxArea) {
maxArea = area;
maxAreaIndex = j;
}
}
lastSelectedDataPoint = bucket[maxAreaIndex];
bucketDataPointIndexes.push(previousBucketsSize + maxAreaIndex);
previousBucketsSize += bucket.length;
}
bucketDataPointIndexes.push(previousBucketsSize);
return bucketDataPointIndexes;
} // Largest triangle three buckets data downsampling algorithm implementation
var createLTTB = config => {
var normalize = createNormalize(config.x, config.y);
return (data, desiredLength) => {
if (desiredLength < 0) {
throw new Error("Supplied negative desiredLength parameter to LTTB: ".concat(desiredLength));
}
var length = data.length;
if (length <= 1 || length <= desiredLength) return data;
var normalizedData = normalize(data);
var buckets = splitIntoBuckets(normalizedData, desiredLength);
var bucketDataPointIndexes = LTTBIndexesForBuckets(buckets);
var output = iterableBasedOn(data, bucketDataPointIndexes.length);
for (var i = 0; i < bucketDataPointIndexes.length; i++) {
output[i] = data[bucketDataPointIndexes[i]];
}
return output;
};
};
var LTTB = createLTTB(createLegacyDataPointConfig());
var mergeBucketAt = (buckets, index) => {
var bucketA = buckets[index];
var bucketB = buckets[index + 1];
if (!bucketA || !bucketB) {
throw new Error("Bucket index out of range for merging: ".concat(index, " (allowed indexes are 0 - ").concat(buckets.length - 2));
}
var mergedBucket = [...bucketA, ...bucketB];
var newBuckets = buckets.slice();
newBuckets.splice(index, 2, mergedBucket);
return newBuckets;
};
var splitBucketAt = (buckets, index) => {
var bucket = buckets[index];
if (!bucket) {
throw new Error("Bucket index out of range for splitting: ".concat(index, " (allowed indexes are 0 - ").concat(buckets.length - 1));
}
var bucketSize = bucket.length;
if (bucketSize < 2) {
return buckets;
}
var bucketALength = Math.ceil(bucketSize / 2);
var bucketA = bucket.slice(0, bucketALength);
var bucketB = bucket.slice(bucketALength);
var newBuckets = buckets.slice();
newBuckets.splice(index, 1, bucketA, bucketB);
return newBuckets;
};
var calculateLinearRegressionCoefficients = data => {
var N = data.length;
var averageX = 0;
var averageY = 0;
for (var i = 0; i < N; i++) {
averageX += data[i][0];
averageY += data[i][1];
}
averageX /= N;
averageY /= N;
var aNumerator = 0;
var aDenominator = 0;
for (var _i = 0; _i < N; _i++) {
var _data$_i = _slicedToArray(data[_i], 2),
x = _data$_i[0],
y = _data$_i[1];
aNumerator += (x - averageX) * (y - averageY);
aDenominator += (x - averageX) * (x - averageX);
}
var a = aNumerator / aDenominator;
var b = averageY - a * averageX;
return [a, b];
};
var calculateSSEForBucket = dataPoints => {
var _calculateLinearRegre = calculateLinearRegressionCoefficients(dataPoints),
_calculateLinearRegre2 = _slicedToArray(_calculateLinearRegre, 2),
a = _calculateLinearRegre2[0],
b = _calculateLinearRegre2[1];
var sumStandardErrorsSquared = 0;
for (var i = 0; i < dataPoints.length; i++) {
var dataPoint = dataPoints[i];
var standardError = dataPoint[1] - (a * dataPoint[0] + b);
sumStandardErrorsSquared += standardError * standardError;
}
return sumStandardErrorsSquared;
};
var calculateSSEForBuckets = buckets => {
// We skip the first and last buckets since they only contain one data point
var sse = [0];
for (var i = 1; i < buckets.length - 1; i++) {
var previousBucket = buckets[i - 1];
var currentBucket = buckets[i];
var nextBucket = buckets[i + 1];
var bucketWithAdjacentPoints = [previousBucket[previousBucket.length - 1], ...currentBucket, nextBucket[0]];
sse.push(calculateSSEForBucket(bucketWithAdjacentPoints));
}
sse.push(0);
return sse;
};
var findLowestSSEAdjacentBucketsIndex = (sse, ignoreIndex) => {
var minSSESum = Number.MAX_VALUE;
var minSSEIndex = undefined;
for (var i = 1; i < sse.length - 2; i++) {
if (i === ignoreIndex || i + 1 === ignoreIndex) {
continue;
}
if (sse[i] + sse[i + 1] < minSSESum) {
minSSESum = sse[i] + sse[i + 1];
minSSEIndex = i;
}
}
return minSSEIndex;
};
var findHighestSSEBucketIndex = (buckets, sse) => {
var maxSSE = 0;
var maxSSEIndex = undefined;
for (var i = 1; i < sse.length - 1; i++) {
if (buckets[i].length > 1 && sse[i] > maxSSE) {
maxSSE = sse[i];
maxSSEIndex = i;
}
}
return maxSSEIndex;
}; // Largest triangle three buckets data downsampling algorithm implementation
var createLTD = config => {
var normalize = createNormalize(config.x, config.y);
return (data, desiredLength) => {
if (desiredLength < 0) {
throw new Error("Supplied negative desiredLength parameter to LTD: ".concat(desiredLength));
}
var length = data.length;
if (length <= 2 || length <= desiredLength) {
return data;
} // Now we are sure that:
//
// - length is [2, Infinity)
// - threshold is (length, Inifnity)
var normalizedData = normalize(data); // Require: data . The original data
// Require: threshold . Number of data points to be returned
// 1: Split the data into equal number of buckets as the threshold but have the first
// bucket only containing the first data point and the last bucket containing only
// the last data point . First and last buckets are then excluded in the bucket
// resizing
// 2: Calculate the SSE for the buckets accordingly . With one point in adjacent
// buckets overlapping
// 3: while halting condition is not met do . For example, using formula 4.2
// 4: Find the bucket F with the highest SSE
// 5: Find the pair of adjacent buckets A and B with the lowest SSE sum . The
// pair should not contain F
// 6: Split bucket F into roughly two equal buckets . If bucket F contains an odd
// number of points then one bucket will contain one more point than the other
// 7: Merge the buckets A and B
// 8: Calculate the SSE of the newly split up and merged buckets
// 9: end while.
// 10: Use the Largest-Triangle-Three-Buckets algorithm on the resulting bucket configuration
// to select one point per buckets
var buckets = splitIntoBuckets(normalizedData, desiredLength);
var numIterations = length * 10 / desiredLength;
for (var iteration = 0; iteration < numIterations; iteration++) {
// 2: Calculate the SSE for the buckets accordingly . With one point in adjacent
// buckets overlapping
var sseForBuckets = calculateSSEForBuckets(buckets); // 4: Find the bucket F with the highest SSE
var highestSSEBucketIndex = findHighestSSEBucketIndex(buckets, sseForBuckets);
if (highestSSEBucketIndex === undefined) {
break;
} // 5: Find the pair of adjacent buckets A and B with the lowest SSE sum . The
// pair should not contain F
var lowestSSEAdajacentBucketIndex = findLowestSSEAdjacentBucketsIndex(sseForBuckets, highestSSEBucketIndex);
if (lowestSSEAdajacentBucketIndex === undefined) {
break;
} // 6: Split bucket F into roughly two equal buckets . If bucket F contains an odd
// number of points then one bucket will contain one more point than the other
buckets = splitBucketAt(buckets, highestSSEBucketIndex); // 7: Merge the buckets A and B
// If the lowest SSE index was after the highest index in the original
// unsplit array then we need to move it by one up since now the array has one more element
// before this index
buckets = mergeBucketAt(buckets, lowestSSEAdajacentBucketIndex > highestSSEBucketIndex ? lowestSSEAdajacentBucketIndex + 1 : lowestSSEAdajacentBucketIndex);
}
var dataPointIndexes = LTTBIndexesForBuckets(buckets);
var outputLength = dataPointIndexes.length;
var output = iterableBasedOn(data, outputLength);
for (var i = 0; i < outputLength; i++) {
output[i] = data[dataPointIndexes[i]];
}
return output;
};
};
var LTD = createLTD(createLegacyDataPointConfig());
exports.LTD = LTD;
exports.calculateLinearRegressionCoefficients = calculateLinearRegressionCoefficients;
exports.calculateSSEForBucket = calculateSSEForBucket;
exports.calculateSSEForBuckets = calculateSSEForBuckets;
exports.createLTD = createLTD;
exports.findHighestSSEBucketIndex = findHighestSSEBucketIndex;
exports.findLowestSSEAdjacentBucketsIndex = findLowestSSEAdjacentBucketsIndex;
exports.mergeBucketAt = mergeBucketAt;
exports.splitBucketAt = splitBucketAt;
//# sourceMappingURL=LTD.js.map