190 lines
6.0 KiB
TypeScript
190 lines
6.0 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import type { WaveformPoint } from '../types'
|
|
import {
|
|
hasMinimumVisibleXValues,
|
|
resolveWaveformRenderingOptions,
|
|
selectDecorationPoints,
|
|
selectRenderablePoints,
|
|
selectSeriesRenderPoints,
|
|
} from './rendering'
|
|
|
|
describe('waveform rendering selection', () => {
|
|
const points = Array.from({ length: 10_000 }, (_, index): WaveformPoint => ({
|
|
x: index,
|
|
y: index === 5_001 ? 10_000 : Math.sin(index / 20),
|
|
}))
|
|
|
|
it('clips to the visible domain and retains one continuity point on each side', () => {
|
|
const selected = selectRenderablePoints(
|
|
points,
|
|
[4_000, 4_100],
|
|
500,
|
|
resolveWaveformRenderingOptions({ downsample: false }),
|
|
)
|
|
|
|
expect(selected[0].x).toBe(3_999)
|
|
expect(selected.at(-1)?.x).toBe(4_101)
|
|
})
|
|
|
|
it('bounds path density while preserving narrow extrema', () => {
|
|
const selected = selectRenderablePoints(
|
|
points,
|
|
[0, 9_999],
|
|
100,
|
|
resolveWaveformRenderingOptions({ downsampleThreshold: 100, maxPointsPerPixel: 4 }),
|
|
)
|
|
|
|
expect(selected.length).toBeLessThanOrEqual(402)
|
|
expect(selected).toContain(points[5_001])
|
|
expect(selected[0]).toBe(points[0])
|
|
expect(selected.at(-1)).toBe(points.at(-1))
|
|
})
|
|
|
|
it('normalizes invalid rendering options to stable defaults', () => {
|
|
expect(
|
|
resolveWaveformRenderingOptions({
|
|
downsampleThreshold: -1,
|
|
maxPointsPerPixel: 0,
|
|
pointMinSpacing: -1,
|
|
errorBarMinSpacing: Number.POSITIVE_INFINITY,
|
|
}),
|
|
).toEqual({
|
|
downsample: true,
|
|
downsampleThreshold: 2_000,
|
|
maxPointsPerPixel: 4,
|
|
pointMinSpacing: 10,
|
|
errorBarMinSpacing: 12,
|
|
})
|
|
})
|
|
|
|
it('accepts custom decoration spacing and uses zero to disable it', () => {
|
|
expect(resolveWaveformRenderingOptions({ pointMinSpacing: 6, errorBarMinSpacing: 0 })).toEqual({
|
|
downsample: true,
|
|
downsampleThreshold: 2_000,
|
|
maxPointsPerPixel: 4,
|
|
pointMinSpacing: 6,
|
|
errorBarMinSpacing: 0,
|
|
})
|
|
})
|
|
|
|
it('selects evenly distributed source points for dense decorations', () => {
|
|
const selected = selectDecorationPoints(points, [0, 9_999], 100, 10, true)
|
|
|
|
expect(selected.length).toBeLessThanOrEqual(12)
|
|
expect(selected[0]).toBe(points[0])
|
|
expect(selected.at(-1)).toBe(points.at(-1))
|
|
expect(selected.every((point) => points.includes(point))).toBe(true)
|
|
})
|
|
|
|
it('clips decorations exactly to the visible domain and preserves sparse points', () => {
|
|
const sparsePoints = [
|
|
{ x: 0, y: 0 },
|
|
{ x: 40, y: 1 },
|
|
{ x: 80, y: 2 },
|
|
{ x: 120, y: 3 },
|
|
]
|
|
|
|
expect(selectDecorationPoints(sparsePoints, [40, 80], 100, 10, true)).toEqual([
|
|
sparsePoints[1],
|
|
sparsePoints[2],
|
|
])
|
|
})
|
|
|
|
it('supports filtering decoration candidates and disabling sampling', () => {
|
|
const errorPoints = Array.from({ length: 100 }, (_, index) => ({
|
|
x: index,
|
|
y: index,
|
|
error: index % 10 === 0 ? 1 : 0,
|
|
}))
|
|
const hasError = (point: WaveformPoint) => (point.error ?? 0) > 0
|
|
|
|
expect(selectDecorationPoints(errorPoints, [0, 99], 100, 12, true, hasError)).toHaveLength(10)
|
|
expect(selectDecorationPoints(errorPoints, [0, 99], 100, 12, false)).toHaveLength(100)
|
|
expect(selectDecorationPoints(errorPoints, [0, 99], 100, 0, true)).toHaveLength(100)
|
|
})
|
|
|
|
it('prefers priority candidates within dense decoration buckets', () => {
|
|
const priorityPoints = Array.from({ length: 100 }, (_, index) => ({
|
|
x: index,
|
|
y: index,
|
|
error: index % 20 === 1 ? 1 : 0,
|
|
}))
|
|
const hasError = (point: WaveformPoint) => (point.error ?? 0) > 0
|
|
const selected = selectDecorationPoints(
|
|
priorityPoints,
|
|
[0, 99],
|
|
100,
|
|
20,
|
|
true,
|
|
undefined,
|
|
hasError,
|
|
)
|
|
|
|
expect(selected.filter(hasError)).toEqual(priorityPoints.filter(hasError))
|
|
expect(selected.length).toBeLessThanOrEqual(Math.ceil(100 / 20) + 2)
|
|
})
|
|
|
|
it('shares visible-range selection for a dense 100k-point series', () => {
|
|
const densePoints = Array.from({ length: 100_000 }, (_, index): WaveformPoint => ({
|
|
x: index,
|
|
y: index === 50_001 ? 10_000 : Math.sin(index / 20),
|
|
error: index % 1_000 === 0 ? 1 : undefined,
|
|
}))
|
|
const selected = selectSeriesRenderPoints(
|
|
densePoints,
|
|
[99_999, 0],
|
|
500,
|
|
resolveWaveformRenderingOptions({ downsampleThreshold: 100 }),
|
|
{
|
|
lineVisible: true,
|
|
pointVisible: true,
|
|
errorBarVisible: true,
|
|
hasErrorPoints: true,
|
|
},
|
|
)
|
|
|
|
expect(selected.linePoints.length).toBeLessThanOrEqual(2_002)
|
|
expect(selected.linePoints).toContain(densePoints[50_001])
|
|
expect(selected.linePoints[0]).toBe(densePoints[0])
|
|
expect(selected.linePoints.at(-1)).toBe(densePoints.at(-1))
|
|
expect(selected.pointRenderPoints.length).toBeLessThanOrEqual(52)
|
|
expect(selected.errorBarRenderPoints.every((point) => (point.error ?? 0) > 0)).toBe(true)
|
|
})
|
|
|
|
it('counts unique visible x values across series and reversed domains', () => {
|
|
const first = {
|
|
points: [
|
|
{ x: 0, y: 0 },
|
|
{ x: 1, y: 1 },
|
|
],
|
|
}
|
|
const second = {
|
|
points: [
|
|
{ x: 0, y: 2 },
|
|
{ x: 1, y: 3 },
|
|
{ x: 2, y: 4 },
|
|
],
|
|
}
|
|
|
|
expect(hasMinimumVisibleXValues([first, second], [2, 0], 3)).toBe(true)
|
|
expect(hasMinimumVisibleXValues([first, second], [0, 2], 4)).toBe(false)
|
|
expect(hasMinimumVisibleXValues([first, second], [1, 1], 1)).toBe(true)
|
|
expect(hasMinimumVisibleXValues([first, second], [3, 4], 1)).toBe(false)
|
|
})
|
|
|
|
it('stops scanning a 100k-point series after reaching the minimum', () => {
|
|
const source = Array.from({ length: 100_000 }, (_, index) => ({ x: index, y: index }))
|
|
let pointReads = 0
|
|
const points = new Proxy(source, {
|
|
get(target, property, receiver) {
|
|
if (typeof property === 'string' && /^\d+$/.test(property)) pointReads += 1
|
|
return Reflect.get(target, property, receiver)
|
|
},
|
|
})
|
|
|
|
expect(hasMinimumVisibleXValues([{ points }], [0, 99_999], 5)).toBe(true)
|
|
expect(pointReads).toBeLessThan(100)
|
|
})
|
|
})
|