export function decimateTimeSeries>( data: T[], maxPoints: number, valueKeys: string[] = [] ): T[] { const len = data.length; if (maxPoints >= len || maxPoints < 3) { return data; } const getY = (point: T, index: number): number => { if (valueKeys.length === 0) { for (const val of Object.values(point)) { if (typeof val === "number") { return val; } } return index; } let sum = 0; let count = 0; for (const key of valueKeys) { const val = point[key]; if (typeof val === "number") { sum += val; count++; } } return count > 0 ? sum / count : index; }; const sampled: T[] = [data[0] as T]; const bucketSize = (len - 2) / (maxPoints - 2); let previousIndex = 0; for (let i = 0; i < maxPoints - 2; i++) { const rangeStart = Math.floor((i + 1) * bucketSize) + 1; const rangeEnd = Math.min(Math.floor((i + 2) * bucketSize) + 1, len - 1); const nextRangeStart = Math.floor((i + 2) * bucketSize) + 1; const nextRangeEnd = Math.min(Math.floor((i + 3) * bucketSize) + 1, len); const nextCount = Math.max(0, nextRangeEnd - nextRangeStart); let avgX = len - 1; let avgY = getY(data[len - 1] as T, len - 1); if (nextCount > 0) { avgX = 0; avgY = 0; for (let j = nextRangeStart; j < nextRangeEnd; j++) { avgX += j; avgY += getY(data[j] as T, j); } avgX /= nextCount; avgY /= nextCount; } const pointA = data[previousIndex] as T; const ax = previousIndex; const ay = getY(pointA, previousIndex); let maxArea = -1; let maxIndex = rangeStart; for (let j = rangeStart; j < rangeEnd; j++) { const area = Math.abs( (ax - avgX) * (getY(data[j] as T, j) - ay) - (ax - j) * (avgY - ay) ) * 0.5; if (area > maxArea) { maxArea = area; maxIndex = j; } } sampled.push(data[maxIndex] as T); previousIndex = maxIndex; } sampled.push(data[len - 1] as T); return sampled; } /** ~1.5 points per pixel — enough for crisp curves without over-drawing. */ export function maxRenderPointsForWidth(innerWidth: number): number { return Math.max(64, Math.ceil(innerWidth * 1.5)); } /** Bucket OHLC rows into fewer candles while preserving high/low extremes. */ export function decimateOhlcData>( data: T[], maxPoints: number ): T[] { const len = data.length; if (maxPoints >= len || maxPoints < 2) { return data; } const bucketSize = len / maxPoints; const sampled: T[] = []; for (let i = 0; i < maxPoints; i++) { const start = Math.floor(i * bucketSize); const end = Math.min(len, Math.floor((i + 1) * bucketSize)); if (start >= end) { continue; } const bucket = data.slice(start, end); const first = bucket[0] as T; const last = bucket.at(-1) as T; let high = Number.NEGATIVE_INFINITY; let low = Number.POSITIVE_INFINITY; for (const row of bucket) { const rowHigh = row.high; const rowLow = row.low; if (typeof rowHigh === "number" && rowHigh > high) { high = rowHigh; } if (typeof rowLow === "number" && rowLow < low) { low = rowLow; } } sampled.push({ ...last, open: first.open, high: Number.isFinite(high) ? high : last.high, low: Number.isFinite(low) ? low : last.low, close: last.close, } as T); } return sampled; }