feat(chart): add series styling and visibility controls

This commit is contained in:
李启源
2026-07-20 17:57:52 +08:00
parent c9e4dae25f
commit efb685646a
31 changed files with 2356 additions and 209 deletions

View File

@@ -55,6 +55,34 @@ describe('waveform annotation markup', () => {
).toBeNull()
})
it('interpolates start, middle, and end step lines at their visual transitions', () => {
const points = [
{ x: 0, y: 2 },
{ x: 2, y: 10 },
]
expect(interpolateAnnotationPoint(points, 0.5, 'step-start')).toEqual({ x: 0.5, y: 10 })
expect(interpolateAnnotationPoint(points, 0.5, 'step-middle')).toEqual({ x: 0.5, y: 2 })
expect(interpolateAnnotationPoint(points, 1, 'step-middle')).toEqual({ x: 1, y: 10 })
expect(interpolateAnnotationPoint(points, 1.5, 'step-middle')).toEqual({ x: 1.5, y: 10 })
expect(interpolateAnnotationPoint(points, 1, 'step-end')).toEqual({ x: 1, y: 2 })
expect(interpolateAnnotationPoint(points, 1, 'step-after')).toEqual({ x: 1, y: 2 })
expect(interpolateAnnotationPoint(points, 2, 'step-start')).toEqual({ x: 2, y: 10 })
expect(interpolateAnnotationPoint(points, 2, 'step-after')).toEqual({ x: 2, y: 10 })
expect(interpolateAnnotationPoint(points, 1, 'none')).toBeNull()
expect(interpolateAnnotationPoint(points, 2, 'none')).toEqual({ x: 2, y: 10 })
})
it('omits interpolated candidates for point-only series between samples', () => {
const pointOnly = createTrack(0, 'points', 0, [
{ x: 0, y: 2 },
{ x: 2, y: 10 },
])
pointOnly.series.lineType = 'none'
expect(findAnnotationSeriesCandidates([pointOnly], 1, 100, 50)).toEqual([])
})
it('sorts line candidates by screen distance and keeps series metadata', () => {
const first = createTrack(0, 'first', 0, [
{ x: 0, y: 0 },

View File

@@ -1,6 +1,6 @@
import { bisector } from 'd3'
import type { WaveformAnnotation, WaveformAnnotationStyle } from '../../types'
import type { WaveformAnnotation, WaveformAnnotationStyle, WaveformLineType } from '../../types'
import type {
AnnotationBoxLayout,
AnnotationHit,
@@ -46,6 +46,7 @@ const pointBisector = bisector((point: { x: number }) => point.x)
export function interpolateAnnotationPoint(
points: Array<{ x: number; y: number }>,
xValue: number,
lineType: WaveformLineType = 'linear',
): { x: number; y: number } | null {
if (!points.length || !Number.isFinite(xValue)) return null
const first = points[0]
@@ -56,8 +57,16 @@ export function interpolateAnnotationPoint(
const rightIndex = pointBisector.left(points, xValue)
const right = points[Math.min(rightIndex, points.length - 1)]
if (right.x === xValue || rightIndex === 0) return { x: xValue, y: right.y }
if (lineType === 'none') return null
const left = points[rightIndex - 1]
if (lineType === 'step-start') return { x: xValue, y: right.y }
if (lineType === 'step-middle') {
return { x: xValue, y: xValue < (left.x + right.x) / 2 ? left.y : right.y }
}
if (lineType === 'step-end' || lineType === 'step-after') {
return { x: xValue, y: left.y }
}
const xSpan = right.x - left.x
if (xSpan === 0) return { x: xValue, y: right.y }
const ratio = (xValue - left.x) / xSpan
@@ -72,7 +81,7 @@ export function findAnnotationSeriesCandidates(
): AnnotationSeriesCandidate[] {
return tracks
.flatMap((track): AnnotationSeriesCandidate[] => {
const point = interpolateAnnotationPoint(track.series.points, xValue)
const point = interpolateAnnotationPoint(track.series.points, xValue, track.series.lineType)
if (!point) return []
const screenX = track.xScale(point.x)
const screenY = track.top + track.yScale(point.y)

View File

@@ -1,6 +1,6 @@
import type { ScaleLinear } from 'd3'
import type { WaveformAnnotation, WaveformPoint } from '../../types'
import type { WaveformAnnotation, WaveformLineType, WaveformPoint } from '../../types'
export interface AnnotationTrackLayout {
index: number
@@ -9,6 +9,7 @@ export interface AnnotationTrackLayout {
name?: string
color?: string
unit?: string
lineType?: WaveformLineType
points: WaveformPoint[]
}
left?: number