diff --git a/src/utils/__tests__/getRescaler.test.ts b/src/utils/__tests__/getRescaler.test.ts index 02cc3185..56b155c5 100644 --- a/src/utils/__tests__/getRescaler.test.ts +++ b/src/utils/__tests__/getRescaler.test.ts @@ -34,3 +34,32 @@ test('getRescale with wrong min / max, no clamp', () => { }); expect(() => rescaler(2)).toThrowError('Value 2 is out of range [0, 1]'); }); + +test('getRescale with wrong min / max and logartihmic', () => { + const array = [10, 100, 1000, 10000]; + + const rescaler = getRescaler({ + originalMin: 10, + originalMax: 1000, + algorithm: 'logarithmic', + }); + + const rescaledArray = array.map(rescaler); + expect(rescaledArray).toStrictEqual([0, 0.5, 1, 1]); +}); + +test('getRescale with square root', () => { + const array = [25, 36, 49]; + + const rescaler = getRescaler({ + originalMin: 25, + originalMax: 49, + targetMin: 10, + targetMax: 50, + algorithm: 'power', + algorithmOptions: { power: 0.5 }, + }); + + const rescaledArray = array.map(rescaler); + expect(rescaledArray).toStrictEqual([10, 30, 50]); +}); diff --git a/src/utils/getRescaler.ts b/src/utils/getRescaler.ts index f383a574..f3a8f8f2 100644 --- a/src/utils/getRescaler.ts +++ b/src/utils/getRescaler.ts @@ -1,3 +1,5 @@ +type Algorithms = 'linear' | 'logarithmic' | 'power'; + export interface RescalerOptions { /** * The minimum value of the original range @@ -24,33 +26,79 @@ export interface RescalerOptions { * @default true */ clamp?: boolean; + /** + * The algorithm to use for the rescaling + * @default 'linear' + */ + algorithm?: Algorithms; + /** + * Options for the algorithm + * @default {} + */ + algorithmOptions?: Record; } export function getRescaler(options: RescalerOptions = {}) { - const { + let { originalMin = 0, originalMax = 1, targetMin = 0, targetMax = 1, clamp = true, + algorithmOptions = {}, } = options; + const convert = getDataConverter( + options.algorithm || 'linear', + algorithmOptions, + ); + originalMin = convert(originalMin); + originalMax = convert(originalMax); + + const originalRange = originalMax - originalMin; + const targetRange = targetMax - targetMin; + return (value: number) => { - if (value < originalMin) { - if (clamp) return targetMin; - throw new RangeError( - `Value ${value} is out of range [${originalMin}, ${originalMax}]`, - ); - } - if (value > originalMax) { - if (clamp) return targetMax; - throw new RangeError( - `Value ${value} is out of range [${originalMin}, ${originalMax}]`, - ); - } - const originalRange = originalMax - originalMin; - const targetRange = targetMax - targetMin; + value = convert(value); + value = checkRange(value, originalMin, originalMax, clamp); + const valueScaled = (value - originalMin) / originalRange; return targetMin + valueScaled * targetRange; }; } + +function getDataConverter( + kind: Algorithms = 'linear', + options: { power?: number } = {}, +) { + return (value: number) => { + switch (kind) { + case 'linear': + return value; + case 'logarithmic': + return Math.log10(value); + case 'power': + return value ** (options.power || 2); + default: + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions + throw new Error(`Unknown kind ${kind}`); + } + }; +} + +function checkRange( + value: number, + min: number, + max: number, + clamp = true, +): number { + if (value < min) { + if (clamp) return min; + throw new RangeError(`Value ${value} is out of range [${min}, ${max}]`); + } + if (value > max) { + if (clamp) return max; + throw new RangeError(`Value ${value} is out of range [${min}, ${max}]`); + } + return value; +}