diff --git a/src/xy/__tests__/xyMedianYAtXs.test.ts b/src/xy/__tests__/xyMedianYAtXs.test.ts index a3678f6f..4f92fca9 100644 --- a/src/xy/__tests__/xyMedianYAtXs.test.ts +++ b/src/xy/__tests__/xyMedianYAtXs.test.ts @@ -50,6 +50,36 @@ test('window at edges is truncated', () => { expect(result.y).toStrictEqual(new Float64Array([5, 5])); }); +test('symmetric window shrinks at edges', () => { + const data = { + x: [1, 2, 3, 4, 5], + y: [10, 1, 5, 3, 8], + }; + + const result = xyMedianYAtXs(data, [1, 2, 3, 4, 5], { + windowSize: 5, + symmetric: true, + }); + + // index 0: window [10] -> 10 + // index 1: window [10,1,5] -> 5 + // index 2: window [10,1,5,3,8] -> 5 + // index 3: window [5,3,8] -> 5 + // index 4: window [8] -> 8 + expect(result.y).toStrictEqual(new Float64Array([10, 5, 5, 5, 8])); +}); + +test('symmetric window keeps full size away from edges', () => { + const data = { + x: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], + y: [2, 8, 3, 100, 5, 6, 1, 9, 4, 7], + }; + + const result = xyMedianYAtXs(data, [3, 7], { symmetric: true }); + + expect(result.y).toStrictEqual(new Float64Array([5, 5])); +}); + test('does not mutate original Float64Array y data', () => { const y = new Float64Array([10, 1, 5, 3, 8]); const yOriginal = new Float64Array(y); diff --git a/src/xy/xyMedianYAtXs.ts b/src/xy/xyMedianYAtXs.ts index 6bfee427..cf0fd184 100644 --- a/src/xy/xyMedianYAtXs.ts +++ b/src/xy/xyMedianYAtXs.ts @@ -6,6 +6,12 @@ import { xMedian } from '../x/xMedian.ts'; export interface XYMedianYAtXsOptions { /** Number of points in the sliding window. Must be odd. Defaults to `5`. */ windowSize?: number; + /** + * If true, the window shrinks on both sides so that it stays centered on the + * closest index when points are missing on one side (near an edge). + * @default false + */ + symmetric?: boolean; } /** @@ -22,16 +28,24 @@ export function xyMedianYAtXs( xValues: NumberArray, options: XYMedianYAtXsOptions = {}, ): DataXY { - const { windowSize = 5 } = options; + const { windowSize = 5, symmetric = false } = options; const { x, y } = data; const halfWindow = Math.floor(windowSize / 2); + const lastIndex = y.length - 1; const result = new Float64Array(xValues.length); for (let i = 0; i < xValues.length; i++) { const centerIndex = xFindClosestIndex(x, xValues[i]); - const fromIndex = Math.max(0, centerIndex - halfWindow); - const toIndex = Math.min(y.length, centerIndex + halfWindow + 1); + let currentHalfWindow = halfWindow; + if (symmetric) { + if (centerIndex < currentHalfWindow) currentHalfWindow = centerIndex; + if (lastIndex - centerIndex < currentHalfWindow) { + currentHalfWindow = lastIndex - centerIndex; + } + } + const fromIndex = Math.max(0, centerIndex - currentHalfWindow); + const toIndex = Math.min(y.length, centerIndex + currentHalfWindow + 1); result[i] = xMedian(y, { exact: false, fromIndex, toIndex }); }