diff --git a/src/data/DataStore.ts b/src/data/DataStore.ts index 36f7373a74..a0f306bd0e 100644 --- a/src/data/DataStore.ts +++ b/src/data/DataStore.ts @@ -1013,6 +1013,75 @@ class DataStore { return target; } + /** + * Large data down sampling using min-max + * @param {string} valueDimension + * @param {number} rate + */ + minmaxDownSample( + valueDimension: DimensionIndex, + rate: number + ): DataStore { + const target = this.clone([valueDimension], true); + const targetStorage = target._chunks; + + const frameSize = Math.floor(1 / rate); + + const dimStore = targetStorage[valueDimension]; + const len = this.count(); + + // Each frame results in 2 data points, one for min and one for max + const newIndices = new (getIndicesCtor(this._rawCount))(Math.ceil(len / frameSize) * 2); + + let offset = 0; + for (let i = 0; i < len; i += frameSize) { + let minIndex = i; + let minValue = dimStore[this.getRawIndex(minIndex)]; + let maxIndex = i; + let maxValue = dimStore[this.getRawIndex(maxIndex)]; + + let thisFrameSize = frameSize; + // Handle final smaller frame + if (i + frameSize > len) { + thisFrameSize = len - i; + } + // Determine min and max within the current frame + for (let k = 0; k < thisFrameSize; k++) { + const rawIndex = this.getRawIndex(i + k); + const value = dimStore[rawIndex]; + + if (value < minValue) { + minValue = value; + minIndex = i + k; + } + if (value > maxValue) { + maxValue = value; + maxIndex = i + k; + } + } + + const rawMinIndex = this.getRawIndex(minIndex); + const rawMaxIndex = this.getRawIndex(maxIndex); + + // Set the order of the min and max values, based on their ordering in the frame + if (minIndex < maxIndex) { + newIndices[offset++] = rawMinIndex; + newIndices[offset++] = rawMaxIndex; + } + else { + newIndices[offset++] = rawMaxIndex; + newIndices[offset++] = rawMinIndex; + } + } + + target._count = offset; + target._indices = newIndices; + + target._updateGetRawIdx(); + + return target; + } + /** * Large data down sampling on given dimension diff --git a/src/data/SeriesData.ts b/src/data/SeriesData.ts index 9267177778..ecf3e03f81 100644 --- a/src/data/SeriesData.ts +++ b/src/data/SeriesData.ts @@ -254,10 +254,10 @@ class SeriesData< // Methods that create a new list based on this list should be listed here. // Notice that those method should `RETURN` the new list. - TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'lttbDownSample', 'map'] as const; + TRANSFERABLE_METHODS = ['cloneShallow', 'downSample', 'minmaxDownSample', 'lttbDownSample', 'map'] as const; // Methods that change indices of this list should be listed here. CHANGABLE_METHODS = ['filterSelf', 'selectRange'] as const; - DOWNSAMPLE_METHODS = ['downSample', 'lttbDownSample'] as const; + DOWNSAMPLE_METHODS = ['downSample', 'minmaxDownSample', 'lttbDownSample'] as const; /** * @param dimensionsInput.dimensions @@ -1098,6 +1098,23 @@ class SeriesData< return list as SeriesData; } + /** + * Large data down sampling using min-max + * @param {string} valueDimension + * @param {number} rate + */ + minmaxDownSample( + valueDimension: DimensionLoose, + rate: number + ): SeriesData { + const list = cloneListForMapAndSample(this); + list._store = this._store.minmaxDownSample( + this._getStoreDimIndex(valueDimension), + rate + ); + return list as SeriesData; + } + /** * Large data down sampling using largest-triangle-three-buckets * @param {string} valueDimension diff --git a/src/processor/dataSample.ts b/src/processor/dataSample.ts index cbd370f2ba..3c19572e9a 100644 --- a/src/processor/dataSample.ts +++ b/src/processor/dataSample.ts @@ -61,22 +61,6 @@ const samplers: Dictionary = { // NaN will cause illegal axis extent. return isFinite(min) ? min : NaN; }, - minmax: function (frame) { - let turningPointAbsoluteValue = -Infinity; - let turningPointOriginalValue = -Infinity; - - for (let i = 0; i < frame.length; i++) { - const originalValue = frame[i]; - const absoluteValue = Math.abs(originalValue); - - if (absoluteValue > turningPointAbsoluteValue) { - turningPointAbsoluteValue = absoluteValue; - turningPointOriginalValue = originalValue; - } - } - - return isFinite(turningPointOriginalValue) ? turningPointOriginalValue : NaN; - }, // TODO // Median nearest: function (frame) { @@ -115,6 +99,9 @@ export default function dataSample(seriesType: string): StageHandler { if (sampling === 'lttb') { seriesModel.setData(data.lttbDownSample(data.mapDimension(valueAxis.dim), 1 / rate)); } + else if (sampling === 'minmax') { + seriesModel.setData(data.minmaxDownSample(data.mapDimension(valueAxis.dim), 1 / rate)); + } let sampler; if (isString(sampling)) { sampler = samplers[sampling];