perf: 优化波形核心算法与图框网格控制

This commit is contained in:
李启源
2026-07-22 10:56:25 +08:00
parent 21bf0d803e
commit 356f55c9fd
16 changed files with 613 additions and 191 deletions

View File

@@ -11,11 +11,44 @@ import {
describe('waveform grid helpers', () => {
it('normalizes grid counts and uses a two by one default', () => {
expect(normalizeGridOptions()).toEqual({ rowCount: 2, columnCount: 1, showPagination: true })
expect(normalizeGridOptions()).toEqual({
rowCount: 2,
columnCount: 1,
showPagination: true,
trackLines: {},
})
expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
rowCount: 1,
columnCount: 10,
showPagination: true,
trackLines: {},
})
})
it('normalizes per-track grid line visibility with visible defaults', () => {
expect(
normalizeGridOptions({
trackLines: {
voltage: { horizontal: false },
current: { vertical: false },
},
}).trackLines,
).toEqual({
voltage: { horizontal: false, vertical: true },
current: { horizontal: true, vertical: false },
})
})
it('falls back to visible grid lines for invalid runtime values', () => {
const options = {
trackLines: {
voltage: { horizontal: 'invalid', vertical: null },
},
} as unknown as Parameters<typeof normalizeGridOptions>[0]
expect(normalizeGridOptions(options).trackLines.voltage).toEqual({
horizontal: true,
vertical: true,
})
})

View File

@@ -8,12 +8,26 @@ export interface WaveformGridOptions {
rowCount?: number
columnCount?: number
showPagination?: boolean
trackLines?: WaveformGridTrackLines
}
export interface WaveformGridLineOptions {
horizontal?: boolean
vertical?: boolean
}
export type WaveformGridTrackLines = Record<string, WaveformGridLineOptions>
export interface NormalizedWaveformGridLineOptions {
horizontal: boolean
vertical: boolean
}
export interface NormalizedWaveformGridOptions {
rowCount: number
columnCount: number
showPagination: boolean
trackLines: Record<string, NormalizedWaveformGridLineOptions>
}
export interface GridCellGeometry {
@@ -37,10 +51,20 @@ const normalizeCount = (value: unknown, fallback: number) => {
}
export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions {
const trackLines = Object.fromEntries(
Object.entries(options?.trackLines ?? {}).map(([trackId, lines]) => [
trackId,
{
horizontal: typeof lines?.horizontal === 'boolean' ? lines.horizontal : true,
vertical: typeof lines?.vertical === 'boolean' ? lines.vertical : true,
},
]),
)
return {
rowCount: normalizeCount(options?.rowCount, 2),
columnCount: normalizeCount(options?.columnCount, 1),
showPagination: options?.showPagination ?? true,
trackLines,
}
}

View File

@@ -24,6 +24,7 @@ function series(id: string, minimum: number, maximum: number): DisplaySeries {
],
xDomain: [0, 1],
yDomain: [minimum, maximum],
hasErrorPoints: false,
}
}
@@ -61,7 +62,7 @@ function layoutForSeries(
series: sourceTrack,
},
],
grid: { rowCount: 1, columnCount: 1, showPagination: false },
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
displayMode: 'independent',
overlayMode: 'single-axis',
independentTransforms: [transform],
@@ -95,7 +96,7 @@ describe('multi-value Y-axis grouping', () => {
series: sourceTrack,
},
],
grid: { rowCount: 1, columnCount: 1, showPagination: false },
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
displayMode: 'independent',
overlayMode: 'single-axis',
independentTransforms: [zoomIdentity],
@@ -198,7 +199,7 @@ describe('multi-value Y-axis grouping', () => {
series: track([series('left', 0, 254), series('right', 0, 254)]),
},
],
grid: { rowCount: 1, columnCount: 1, showPagination: false },
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
displayMode: 'independent',
overlayMode: 'multi-axis',
independentTransforms: [zoomIdentity],
@@ -241,6 +242,7 @@ describe('decoration sampling', () => {
error: index % 200 === 1 ? 0.1 : 0,
})),
xDomain: [0, 999],
hasErrorPoints: true,
})
it('shares prioritized source points between dense symbols and error bars', () => {
@@ -262,6 +264,7 @@ describe('decoration sampling', () => {
it('keeps standalone, zero-error, and non-downsampled decoration behavior', () => {
const noErrors = denseSeries()
noErrors.points = noErrors.points.map((point) => ({ x: point.x, y: point.y }))
noErrors.hasErrorPoints = false
const zeroErrorPath = layoutForSeries(noErrors)
expect(zeroErrorPath.errorBarRenderPoints).toEqual([])
expect(zeroErrorPath.pointRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 10) + 2)

View File

@@ -9,11 +9,9 @@ import {
} from 'd3'
import {
selectDecorationPoints,
selectRenderablePoints,
resolveWaveformPointErrors,
selectSeriesRenderPoints,
type ResolvedWaveformRenderingOptions,
} from '../../core'
} from '../../core/rendering'
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
import {
buildMinorTicks,
@@ -217,6 +215,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
points: [],
xDomain: [0, 1],
yDomain: [0, 1],
hasErrorPoints: false,
}
const displayTrack: DisplayTrack = cell.series ?? {
id: emptySeries.id,
@@ -317,51 +316,18 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
axis.seriesList.some((series) => series.id === trackSeries.id),
)
const seriesYScale = yAxis?.scale ?? yScale
const pathPoints = selectRenderablePoints(
const renderPoints = selectSeriesRenderPoints(
trackSeries.points,
domain,
cell.width,
options.rendering,
{
lineVisible: !isEmpty && trackSeries.lineType !== 'none',
pointVisible: trackSeries.pointType !== 'none',
errorBarVisible: trackSeries.errorBar.visible,
hasErrorPoints: trackSeries.hasErrorPoints,
},
)
const hasError = (point: WaveformPoint) => {
const { lower, upper } = resolveWaveformPointErrors(point)
return lower !== 0 || upper !== 0
}
const hasErrorPoints = trackSeries.errorBar.visible && trackSeries.points.some(hasError)
const sharesDecorationPoints = trackSeries.pointType !== 'none' && hasErrorPoints
const sharedDecorationPoints = sharesDecorationPoints
? selectDecorationPoints(
trackSeries.points,
domain,
cell.width,
Math.max(options.rendering.pointMinSpacing, options.rendering.errorBarMinSpacing),
options.rendering.downsample,
undefined,
hasError,
)
: undefined
const pointRenderPoints =
trackSeries.pointType === 'none'
? []
: (sharedDecorationPoints ??
selectDecorationPoints(
trackSeries.points,
domain,
cell.width,
options.rendering.pointMinSpacing,
options.rendering.downsample,
))
const errorBarRenderPoints = trackSeries.errorBar.visible
? (sharedDecorationPoints?.filter(hasError) ??
selectDecorationPoints(
trackSeries.points,
domain,
cell.width,
options.rendering.errorBarMinSpacing,
options.rendering.downsample,
hasError,
))
: []
const pathGenerator = line<WaveformPoint>()
.x((point) => xScale(point.x))
.y((point) => seriesYScale(point.y))
@@ -372,9 +338,9 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
}
return {
series: trackSeries,
path: isEmpty || trackSeries.lineType === 'none' ? null : pathGenerator(pathPoints),
pointRenderPoints,
errorBarRenderPoints,
path: renderPoints.linePoints.length ? pathGenerator(renderPoints.linePoints) : null,
pointRenderPoints: renderPoints.pointRenderPoints,
errorBarRenderPoints: renderPoints.errorBarRenderPoints,
yScale: seriesYScale,
yAxisIndex: yAxis?.index ?? 0,
}
@@ -407,6 +373,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
xAxisExponent,
path: seriesPaths[0]?.path ?? null,
seriesPaths,
gridLines: options.grid.trackLines[displayTrack.id] ?? {
horizontal: true,
vertical: true,
},
showXAxis:
(isEmpty || hasVisibleSeries) &&
(options.displayMode === 'independent' ||

View File

@@ -5,6 +5,7 @@ import type {
WaveformPoint,
WaveformPointType,
} from '../../types'
import type { NormalizedWaveformGridLineOptions } from './grid'
/**
* 显示系列
@@ -21,6 +22,7 @@ export interface DisplaySeries {
points: WaveformPoint[]
xDomain: [number, number]
yDomain: [number, number]
hasErrorPoints: boolean
}
export interface DisplayTrack {
@@ -97,6 +99,7 @@ export interface TrackLayout {
path: string | null
seriesPaths: TrackSeriesPath[]
showXAxis: boolean
gridLines: NormalizedWaveformGridLineOptions
}
// 重新导出 WaveformPoint 方便使用

View File

@@ -22,34 +22,42 @@ export interface PreparedWaveformSeries {
points: WaveformPoint[]
xDomain: [number, number]
yDomain: [number, number]
hasErrorPoints: boolean
}
function pointDomain(
function pointMetrics(
points: WaveformPoint[],
key: 'x' | 'y',
includeErrors = false,
): [number, number] {
let minimum = Number.POSITIVE_INFINITY
let maximum = Number.NEGATIVE_INFINITY
points.forEach((point) => {
const value = point[key]
if (value < minimum) minimum = value
if (value > maximum) maximum = value
if (key === 'y' && includeErrors) {
): { xDomain: [number, number]; yDomain: [number, number]; hasErrorPoints: boolean } {
let xMinimum = Number.POSITIVE_INFINITY
let xMaximum = Number.NEGATIVE_INFINITY
let yMinimum = Number.POSITIVE_INFINITY
let yMaximum = Number.NEGATIVE_INFINITY
let hasErrorPoints = false
for (const point of points) {
if (point.x < xMinimum) xMinimum = point.x
if (point.x > xMaximum) xMaximum = point.x
if (point.y < yMinimum) yMinimum = point.y
if (point.y > yMaximum) yMaximum = point.y
if (includeErrors) {
const errors = resolveWaveformPointErrors(point)
minimum = Math.min(minimum, point.y - errors.lower)
maximum = Math.max(maximum, point.y + errors.upper)
if (errors.lower !== 0 || errors.upper !== 0) hasErrorPoints = true
yMinimum = Math.min(yMinimum, point.y - errors.lower)
yMaximum = Math.max(yMaximum, point.y + errors.upper)
}
})
return paddedDomain(Number.isFinite(minimum) ? [minimum, maximum] : [])
}
return {
xDomain: paddedDomain(Number.isFinite(xMinimum) ? [xMinimum, xMaximum] : []),
yDomain: paddedDomain(Number.isFinite(yMinimum) ? [yMinimum, yMaximum] : []),
hasErrorPoints,
}
}
export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSeries[] {
return normalizeWaveformSeries(data).map((series) => ({
...series,
xDomain: pointDomain(series.points, 'x'),
yDomain: pointDomain(series.points, 'y', series.errorBar.visible),
}))
return normalizeWaveformSeries(data).map((series) => {
const metrics = pointMetrics(series.points, series.errorBar.visible)
return { ...series, ...metrics }
})
}
export function usePreparedWaveformSeries(data: () => WaveformData, onDataChange: () => void) {