109 lines
3.6 KiB
TypeScript
109 lines
3.6 KiB
TypeScript
|
|
import { bisector } from 'd3'
|
||
|
|
|
||
|
|
import type { WaveformPoint, WaveformRenderingOptions } from '../types'
|
||
|
|
|
||
|
|
export interface ResolvedWaveformRenderingOptions {
|
||
|
|
downsample: boolean
|
||
|
|
downsampleThreshold: number
|
||
|
|
maxPointsPerPixel: number
|
||
|
|
}
|
||
|
|
|
||
|
|
export const DEFAULT_WAVEFORM_RENDERING_OPTIONS: ResolvedWaveformRenderingOptions = {
|
||
|
|
downsample: true,
|
||
|
|
downsampleThreshold: 2_000,
|
||
|
|
maxPointsPerPixel: 4,
|
||
|
|
}
|
||
|
|
|
||
|
|
const pointBisector = bisector((point: WaveformPoint) => point.x)
|
||
|
|
|
||
|
|
export function resolveWaveformRenderingOptions(
|
||
|
|
options?: WaveformRenderingOptions,
|
||
|
|
): ResolvedWaveformRenderingOptions {
|
||
|
|
const threshold = Number(options?.downsampleThreshold)
|
||
|
|
const pointsPerPixel = Number(options?.maxPointsPerPixel)
|
||
|
|
return {
|
||
|
|
downsample: options?.downsample ?? DEFAULT_WAVEFORM_RENDERING_OPTIONS.downsample,
|
||
|
|
downsampleThreshold:
|
||
|
|
Number.isFinite(threshold) && threshold >= 2
|
||
|
|
? Math.floor(threshold)
|
||
|
|
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.downsampleThreshold,
|
||
|
|
maxPointsPerPixel:
|
||
|
|
Number.isFinite(pointsPerPixel) && pointsPerPixel > 0
|
||
|
|
? pointsPerPixel
|
||
|
|
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.maxPointsPerPixel,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushUniquePoint(target: WaveformPoint[], point: WaveformPoint | undefined) {
|
||
|
|
if (point && target[target.length - 1] !== point) target.push(point)
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Select the visible source range and preserve first/min/max/last values in each X bucket.
|
||
|
|
* Source points must be sorted by X.
|
||
|
|
*/
|
||
|
|
export function selectRenderablePoints(
|
||
|
|
points: WaveformPoint[],
|
||
|
|
domain: [number, number],
|
||
|
|
width: number,
|
||
|
|
options: ResolvedWaveformRenderingOptions,
|
||
|
|
): WaveformPoint[] {
|
||
|
|
if (!points.length || width <= 0) return []
|
||
|
|
|
||
|
|
const domainStart = Math.min(domain[0], domain[1])
|
||
|
|
const domainEnd = Math.max(domain[0], domain[1])
|
||
|
|
const visibleStart = pointBisector.left(points, domainStart)
|
||
|
|
const visibleEnd = pointBisector.right(points, domainEnd)
|
||
|
|
const start = Math.max(0, visibleStart - 1)
|
||
|
|
const end = Math.min(points.length, visibleEnd + 1)
|
||
|
|
const visibleCount = end - start
|
||
|
|
if (visibleCount <= 0) return []
|
||
|
|
if (!options.downsample || visibleCount <= options.downsampleThreshold) {
|
||
|
|
return points.slice(start, end)
|
||
|
|
}
|
||
|
|
|
||
|
|
const maximumPointCount = Math.max(4, Math.floor(width * options.maxPointsPerPixel))
|
||
|
|
const bucketCount = Math.max(1, Math.floor(maximumPointCount / 4))
|
||
|
|
if (visibleCount <= maximumPointCount) return points.slice(start, end)
|
||
|
|
|
||
|
|
const result: WaveformPoint[] = []
|
||
|
|
const span = domainEnd - domainStart || 1
|
||
|
|
let activeBucket = -1
|
||
|
|
let firstIndex = -1
|
||
|
|
let lastIndex = -1
|
||
|
|
let minimumIndex = -1
|
||
|
|
let maximumIndex = -1
|
||
|
|
|
||
|
|
const flushBucket = () => {
|
||
|
|
if (firstIndex < 0) return
|
||
|
|
const indexes = [firstIndex, minimumIndex, maximumIndex, lastIndex]
|
||
|
|
.filter((index, position, source) => index >= 0 && source.indexOf(index) === position)
|
||
|
|
.sort((left, right) => left - right)
|
||
|
|
indexes.forEach((index) => pushUniquePoint(result, points[index]))
|
||
|
|
}
|
||
|
|
|
||
|
|
pushUniquePoint(result, points[start])
|
||
|
|
for (let index = Math.max(start, visibleStart); index < Math.min(end, visibleEnd); index += 1) {
|
||
|
|
const point = points[index]
|
||
|
|
const bucket = Math.min(
|
||
|
|
bucketCount - 1,
|
||
|
|
Math.max(0, Math.floor(((point.x - domainStart) / span) * bucketCount)),
|
||
|
|
)
|
||
|
|
if (bucket !== activeBucket) {
|
||
|
|
flushBucket()
|
||
|
|
activeBucket = bucket
|
||
|
|
firstIndex = index
|
||
|
|
lastIndex = index
|
||
|
|
minimumIndex = index
|
||
|
|
maximumIndex = index
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
lastIndex = index
|
||
|
|
if (point.y < points[minimumIndex].y) minimumIndex = index
|
||
|
|
if (point.y > points[maximumIndex].y) maximumIndex = index
|
||
|
|
}
|
||
|
|
flushBucket()
|
||
|
|
pushUniquePoint(result, points[end - 1])
|
||
|
|
return result
|
||
|
|
}
|