From 88be1878cc54ca2ba80394fc0496594773a2f1d2 Mon Sep 17 00:00:00 2001 From: Luc Patiny Date: Thu, 9 Jul 2026 07:27:49 +0200 Subject: [PATCH] perf: avoid extra array copies in xy median and scan helpers - xMedian: add optional fromIndex/toIndex range so it slices only the needed window internally - xyMedianY / xyMedianYAtXs: pass the range to xMedian instead of building a per-point window (removes the double copy for plain-array input) - xyAlign: compute the weighted x only for the 'weighted' option - xyMaxY / xyMaxYPoint / xyMinYPoint: start the scan after the seed index - xyMedian: guard the exact-half branch against an out-of-bounds read at the last index --- src/x/xMedian.ts | 24 ++++++++++++++++++++---- src/xy/xyAlign.ts | 9 ++++----- src/xy/xyMaxY.ts | 2 +- src/xy/xyMaxYPoint.ts | 2 +- src/xy/xyMedian.ts | 1 + src/xy/xyMedianY.ts | 5 +---- src/xy/xyMedianYAtXs.ts | 5 +---- src/xy/xyMinYPoint.ts | 2 +- 8 files changed, 30 insertions(+), 20 deletions(-) diff --git a/src/x/xMedian.ts b/src/x/xMedian.ts index 99bcc9ff0..9200a32fe 100644 --- a/src/x/xMedian.ts +++ b/src/x/xMedian.ts @@ -8,6 +8,18 @@ interface XMedianOptions { * @default false */ exact?: boolean; + + /** + * First index (inclusive) of the range to consider. + * @default 0 + */ + fromIndex?: number; + + /** + * Last index (exclusive) of the range to consider. + * @default input.length + */ + toIndex?: number; } /** @@ -24,13 +36,17 @@ export function xMedian( throw new TypeError('input must be an array'); } - if (input.length === 0) { + const { + exact = false, + fromIndex = 0, + toIndex = input.length, + } = options || {}; + const array = input.slice(fromIndex, toIndex); + + if (array.length === 0) { throw new TypeError('input must not be empty'); } - const { exact = false } = options || {}; - const array = input.slice(); - const middleIndex = calcMiddle(0, array.length - 1); const median = quickSelect(array, middleIndex); diff --git a/src/xy/xyAlign.ts b/src/xy/xyAlign.ts index bc1ab09d6..d88c84f2d 100644 --- a/src/xy/xyAlign.ts +++ b/src/xy/xyAlign.ts @@ -99,10 +99,6 @@ export function xyAlign( i++; } } else { - const weightedX = - (data1.x[i] * data1.y[i] + data2.x[j] * data2.y[j]) / - (data1.y[i] + data2.y[j]); - switch (x) { case 'x1': result.x.push(data1.x[i]); @@ -111,7 +107,10 @@ export function xyAlign( result.x.push(data2.x[j]); break; case 'weighted': - result.x.push(weightedX); + result.x.push( + (data1.x[i] * data1.y[i] + data2.x[j] * data2.y[j]) / + (data1.y[i] + data2.y[j]), + ); break; default: throw new Error(`unknown x option value: ${String(x)}`); diff --git a/src/xy/xyMaxY.ts b/src/xy/xyMaxY.ts index 4f7e42a49..376e58bf3 100644 --- a/src/xy/xyMaxY.ts +++ b/src/xy/xyMaxY.ts @@ -42,7 +42,7 @@ export function xyMaxY(data: DataXY, options: XYMaxYOptions = {}): number { const { fromIndex, toIndex } = xGetFromToIndex(x, options); let currentxyMaxY = y[fromIndex]; - for (let i = fromIndex; i <= toIndex; i++) { + for (let i = fromIndex + 1; i <= toIndex; i++) { if (y[i] > currentxyMaxY) currentxyMaxY = y[i]; } diff --git a/src/xy/xyMaxYPoint.ts b/src/xy/xyMaxYPoint.ts index 921b37231..bc37e9d4a 100644 --- a/src/xy/xyMaxYPoint.ts +++ b/src/xy/xyMaxYPoint.ts @@ -48,7 +48,7 @@ export function xyMaxYPoint( const { fromIndex, toIndex } = xGetFromToIndex(x, options); let current = { x: x[fromIndex], y: y[fromIndex], index: fromIndex }; - for (let i = fromIndex; i <= toIndex; i++) { + for (let i = fromIndex + 1; i <= toIndex; i++) { if (y[i] > current.y) current = { x: x[i], y: y[i], index: i }; } diff --git a/src/xy/xyMedian.ts b/src/xy/xyMedian.ts index c03af7394..85929b917 100644 --- a/src/xy/xyMedian.ts +++ b/src/xy/xyMedian.ts @@ -31,6 +31,7 @@ export function xyMedian(data: DataXY): number { if (cumSumY > sumY / 2) { return x[i]; } else if (cumSumY === sumY / 2) { + if (i === x.length - 1) return x[i]; return 0.5 * (x[i] + x[i + 1]); } } diff --git a/src/xy/xyMedianY.ts b/src/xy/xyMedianY.ts index 40fd82838..779397a0a 100644 --- a/src/xy/xyMedianY.ts +++ b/src/xy/xyMedianY.ts @@ -26,10 +26,7 @@ export function xyMedianY( for (let i = 0; i < y.length; i++) { const fromIndex = Math.max(0, i - halfWindow); const toIndex = Math.min(y.length, i + halfWindow + 1); - const window = ArrayBuffer.isView(y) - ? y.subarray(fromIndex, toIndex) - : y.slice(fromIndex, toIndex); - result[i] = xMedian(window, { exact: false }); + result[i] = xMedian(y, { exact: false, fromIndex, toIndex }); } return { x, y: result }; diff --git a/src/xy/xyMedianYAtXs.ts b/src/xy/xyMedianYAtXs.ts index 34fc7528e..6bfee4277 100644 --- a/src/xy/xyMedianYAtXs.ts +++ b/src/xy/xyMedianYAtXs.ts @@ -32,10 +32,7 @@ export function xyMedianYAtXs( const centerIndex = xFindClosestIndex(x, xValues[i]); const fromIndex = Math.max(0, centerIndex - halfWindow); const toIndex = Math.min(y.length, centerIndex + halfWindow + 1); - const window = ArrayBuffer.isView(y) - ? y.subarray(fromIndex, toIndex) - : y.slice(fromIndex, toIndex); - result[i] = xMedian(window, { exact: false }); + result[i] = xMedian(y, { exact: false, fromIndex, toIndex }); } return { x: xValues, y: result }; diff --git a/src/xy/xyMinYPoint.ts b/src/xy/xyMinYPoint.ts index 950445dfd..89d9c2c95 100644 --- a/src/xy/xyMinYPoint.ts +++ b/src/xy/xyMinYPoint.ts @@ -49,7 +49,7 @@ export function xyMinYPoint( y: y[fromIndex], index: fromIndex, }; - for (let i = fromIndex; i <= toIndex; i++) { + for (let i = fromIndex + 1; i <= toIndex; i++) { if (y[i] < current.y) current = { x: x[i], y: y[i], index: i }; }