waveform-analysis 0.1.48 → 0.1.50

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +120 -5
  2. package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +2683 -1153
  5. package/dist/style.css +1 -1
  6. package/dist/types/components/WaveformChart.vue.d.ts +14 -0
  7. package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
  8. package/dist/types/components/annotation/markup.d.ts +5 -6
  9. package/dist/types/components/annotation/types.d.ts +2 -0
  10. package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
  11. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  12. package/dist/types/components/core/types.d.ts +2 -0
  13. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  14. package/dist/types/components/core/useWaveformData.d.ts +2 -0
  15. package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
  16. package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
  17. package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
  18. package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
  19. package/dist/types/components/data/types.d.ts +1 -1
  20. package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
  21. package/dist/types/components/waveform.d.ts +1 -1
  22. package/dist/types/core/compactSamplePointSource.d.ts +3 -0
  23. package/dist/types/core/data.d.ts +7 -1
  24. package/dist/types/core/index.d.ts +2 -1
  25. package/dist/types/core/rendering.d.ts +3 -1
  26. package/dist/types/core/renderingOptions.d.ts +11 -1
  27. package/dist/types/core/samplingReference.d.ts +36 -0
  28. package/dist/types/core/samplingReferenceValues.d.ts +4 -0
  29. package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
  30. package/dist/types/core/wasmSampling.d.ts +48 -0
  31. package/dist/types/core/waveformPointSource.d.ts +53 -0
  32. package/dist/types/core/workerSampling/index.d.ts +5 -0
  33. package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
  34. package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
  35. package/dist/types/core/workerSampling/protocol.d.ts +153 -0
  36. package/dist/types/core/workerSampling/repository.d.ts +36 -0
  37. package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
  38. package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
  39. package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
  40. package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
  41. package/dist/types/index.d.ts +2 -2
  42. package/dist/types/types/chart.d.ts +56 -0
  43. package/dist/types/types/data.d.ts +20 -2
  44. package/dist/types/types/index.d.ts +2 -2
  45. package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
  46. package/package.json +5 -4
  47. package/src/App.test.ts +11 -7
  48. package/src/App.vue +2 -2
  49. package/src/DemoRouterApp.vue +1 -0
  50. package/src/components/WaveformChart.css +2 -0
  51. package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
  52. package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
  53. package/src/components/annotation/markup.ts +45 -26
  54. package/src/components/annotation/types.ts +2 -0
  55. package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
  56. package/src/components/core/latestTaskScheduler.test.ts +51 -0
  57. package/src/components/core/latestTaskScheduler.ts +75 -0
  58. package/src/components/core/trackLayoutBuilder.ts +3 -0
  59. package/src/components/core/types.ts +2 -0
  60. package/src/components/core/useWaveformChartController.ts +14 -0
  61. package/src/components/core/useWaveformData.test.ts +95 -0
  62. package/src/components/core/useWaveformData.ts +23 -32
  63. package/src/components/core/useWaveformLayout.ts +4 -4
  64. package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
  65. package/src/components/core/useWaveformRenderSampling.ts +394 -0
  66. package/src/components/core/waveformChartTypes.ts +13 -0
  67. package/src/components/core/workerSamplingClient.ts +148 -0
  68. package/src/components/data/types.ts +9 -0
  69. package/src/components/interaction/WaveformTooltip.test.ts +1 -3
  70. package/src/components/interaction/WaveformTooltip.vue +13 -12
  71. package/src/components/interaction/useWaveformHover.ts +7 -7
  72. package/src/components/interaction/zoomConstraints.ts +5 -1
  73. package/src/components/waveform.ts +9 -0
  74. package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
  75. package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
  76. package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
  77. package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
  78. package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
  79. package/src/core/compactSamplePointSource.ts +76 -0
  80. package/src/core/data.test.ts +114 -0
  81. package/src/core/data.ts +177 -75
  82. package/src/core/index.ts +11 -0
  83. package/src/core/rendering.test.ts +87 -12
  84. package/src/core/rendering.ts +35 -1
  85. package/src/core/renderingOptions.ts +124 -15
  86. package/src/core/samplingReference.test.ts +124 -0
  87. package/src/core/samplingReference.ts +243 -0
  88. package/src/core/samplingReferenceValues.ts +169 -0
  89. package/src/core/samplingReferenceVectors.ts +46 -0
  90. package/src/core/wasmSampling.test.ts +205 -0
  91. package/src/core/wasmSampling.ts +230 -0
  92. package/src/core/waveformPointSource.ts +395 -0
  93. package/src/core/workerSampling/index.ts +30 -0
  94. package/src/core/workerSampling/javascriptBackend.ts +15 -0
  95. package/src/core/workerSampling/multiresolution.test.ts +69 -0
  96. package/src/core/workerSampling/multiresolution.ts +300 -0
  97. package/src/core/workerSampling/protocol.test.ts +34 -0
  98. package/src/core/workerSampling/protocol.ts +207 -0
  99. package/src/core/workerSampling/repository.test.ts +400 -0
  100. package/src/core/workerSampling/repository.ts +400 -0
  101. package/src/core/workerSampling/repositoryCache.test.ts +163 -0
  102. package/src/core/workerSampling/repositoryDataset.ts +194 -0
  103. package/src/core/workerSampling/repositorySupport.ts +225 -0
  104. package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
  105. package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
  106. package/src/core/workerSampling/samplingCache.ts +94 -0
  107. package/src/core/workerSampling/workerRuntime.ts +40 -0
  108. package/src/data/simulatedWaveforms.test.ts +19 -0
  109. package/src/data/simulatedWaveforms.ts +11 -8
  110. package/src/demoRoutes.css +177 -0
  111. package/src/index.ts +18 -0
  112. package/src/router.ts +5 -0
  113. package/src/types/chart.ts +63 -0
  114. package/src/types/data.ts +24 -1
  115. package/src/types/index.ts +9 -0
  116. package/src/views/WasmSamplingDemo.test.ts +13 -0
  117. package/src/views/WasmSamplingDemo.vue +180 -0
@@ -1,6 +1,9 @@
1
- import { bisector } from 'd3'
2
-
3
1
  import type { WaveformAnnotation, WaveformAnnotationStyle, WaveformLineType } from '../../types'
2
+ import {
3
+ isWaveformPointSource,
4
+ pointSourceFromPoints,
5
+ type WaveformPointSource,
6
+ } from '../../core/waveformPointSource'
4
7
  import type { AnnotationHit, AnnotationSeriesCandidate, AnnotationTrackLayout } from './types'
5
8
 
6
9
  export const DEFAULT_ANNOTATION_STYLE = {
@@ -23,16 +26,26 @@ export const ANNOTATION_TEXT_GLYPH_HEIGHT = 14
23
26
  export const ANNOTATION_TEXT_FONT = '12px Arial, sans-serif'
24
27
  export const ANNOTATION_CONNECTOR_LENGTH = 32
25
28
 
26
- const pointBisector = bisector((point: { x: number }) => point.x)
29
+ type PointCollection = Array<{ x: number; y: number }> | WaveformPointSource
30
+
31
+ function sourceFor(points: PointCollection) {
32
+ return isWaveformPointSource(points) ? points : pointSourceFromPoints(points)
33
+ }
27
34
 
28
35
  function findNearestPointOnScreen(
29
36
  track: AnnotationTrackLayout,
30
37
  pointerX: number,
31
38
  pointerY: number,
32
39
  ): { point: { x: number; y: number }; screenX: number; screenY: number; distance: number } | null {
33
- let nearest: { point: { x: number; y: number }; screenX: number; screenY: number; distance: number } | null =
34
- null
35
- for (const point of track.series.points) {
40
+ let nearest: {
41
+ point: { x: number; y: number }
42
+ screenX: number
43
+ screenY: number
44
+ distance: number
45
+ } | null = null
46
+ const source = track.series.source ?? pointSourceFromPoints(track.series.points)
47
+ for (let index = 0; index < source.length; index += 1) {
48
+ const point = source.pointAt(index)!
36
49
  if (!Number.isFinite(point.x) || !Number.isFinite(point.y)) continue
37
50
  const screenX = track.xScale(point.x)
38
51
  const screenY = track.top + track.yScale(point.y)
@@ -45,33 +58,35 @@ function findNearestPointOnScreen(
45
58
  }
46
59
 
47
60
  export function findNearestPointByX(
48
- points: Array<{ x: number; y: number }>,
61
+ points: PointCollection,
49
62
  xValue: number,
50
63
  ): { x: number; y: number } | null {
51
- if (!points.length || !Number.isFinite(xValue)) return null
52
- const centerIndex = pointBisector.center(points, xValue)
53
- const center = points[Math.min(centerIndex, points.length - 1)]
54
- const left = points[Math.max(0, centerIndex - 1)]
55
- return Math.abs(xValue - left.x) < Math.abs(center.x - xValue) ? left : center
64
+ const source = sourceFor(points)
65
+ if (!source.length || !Number.isFinite(xValue)) return null
66
+ const rightIndex = source.visibleRange([xValue, xValue]).start
67
+ const right = source.pointAt(Math.min(rightIndex, source.length - 1))!
68
+ const left = source.pointAt(Math.max(0, rightIndex - 1))!
69
+ return Math.abs(xValue - left.x) < Math.abs(right.x - xValue) ? left : right
56
70
  }
57
71
 
58
72
  export function interpolateAnnotationPoint(
59
- points: Array<{ x: number; y: number }>,
73
+ points: PointCollection,
60
74
  xValue: number,
61
75
  lineType: WaveformLineType = 'linear',
62
76
  ): { x: number; y: number } | null {
63
- if (!points.length || !Number.isFinite(xValue)) return null
64
- const first = points[0]
65
- const last = points[points.length - 1]
77
+ const source = sourceFor(points)
78
+ if (!source.length || !Number.isFinite(xValue)) return null
79
+ const first = source.pointAt(0)!
80
+ const last = source.pointAt(source.length - 1)!
66
81
  if (xValue < first.x || xValue > last.x) return null
67
- if (points.length === 1) return xValue === first.x ? { ...first } : null
82
+ if (source.length === 1) return xValue === first.x ? { ...first } : null
68
83
 
69
- const rightIndex = pointBisector.left(points, xValue)
70
- const right = points[Math.min(rightIndex, points.length - 1)]
84
+ const rightIndex = source.visibleRange([xValue, xValue]).start
85
+ const right = source.pointAt(Math.min(rightIndex, source.length - 1))!
71
86
  if (right.x === xValue || rightIndex === 0) return { x: xValue, y: right.y }
72
87
  if (lineType === 'none') return null
73
88
 
74
- const left = points[rightIndex - 1]
89
+ const left = source.pointAt(rightIndex - 1)!
75
90
  if (lineType === 'step-start') return { x: xValue, y: right.y }
76
91
  if (lineType === 'step-middle') {
77
92
  return { x: xValue, y: xValue < (left.x + right.x) / 2 ? left.y : right.y }
@@ -108,7 +123,7 @@ export function findAnnotationSeriesCandidates(
108
123
  ]
109
124
  }
110
125
  const interpolatedPoint = interpolateAnnotationPoint(
111
- track.series.points,
126
+ track.series.source ?? pointSourceFromPoints(track.series.points),
112
127
  xValue,
113
128
  track.series.lineType,
114
129
  )
@@ -116,7 +131,10 @@ export function findAnnotationSeriesCandidates(
116
131
 
117
132
  // Always snap to nearest actual sample point for the anchor
118
133
  // This ensures annotations align with visible data points
119
- const nearestPoint = findNearestPointByX(track.series.points, xValue)
134
+ const nearestPoint = findNearestPointByX(
135
+ track.series.source ?? pointSourceFromPoints(track.series.points),
136
+ xValue,
137
+ )
120
138
  if (!nearestPoint) return []
121
139
 
122
140
  // Use interpolated point for distance calculation to get accurate series selection
@@ -213,15 +231,16 @@ export function findNearestAnnotationPoint(
213
231
 
214
232
  tracks.forEach((track) => {
215
233
  const localY = pointerY - track.top
216
- if (localY < 0 || localY > track.height || track.series.points.length === 0) return
234
+ const source = track.series.source ?? pointSourceFromPoints(track.series.points)
235
+ if (localY < 0 || localY > track.height || source.length === 0) return
217
236
 
218
237
  const xValue = track.xScale.invert(pointerX)
219
- const centerIndex = pointBisector.center(track.series.points, xValue)
238
+ const centerIndex = source.visibleRange([xValue, xValue]).start
220
239
  const firstIndex = Math.max(0, centerIndex - 2)
221
- const lastIndex = Math.min(track.series.points.length - 1, centerIndex + 2)
240
+ const lastIndex = Math.min(source.length - 1, centerIndex + 2)
222
241
 
223
242
  for (let candidateIndex = firstIndex; candidateIndex <= lastIndex; candidateIndex += 1) {
224
- const point = track.series.points[candidateIndex]
243
+ const point = source.pointAt(candidateIndex)!
225
244
  const screenX = track.xScale(point.x)
226
245
  const screenY = track.yScale(point.y) + track.top
227
246
  const distance = Math.hypot(screenX - pointerX, screenY - pointerY)
@@ -1,6 +1,7 @@
1
1
  import type { ScaleLinear } from 'd3'
2
2
 
3
3
  import type { WaveformAnnotation, WaveformLineType, WaveformPoint } from '../../types'
4
+ import type { WaveformPointSource } from '../../core/waveformPointSource'
4
5
 
5
6
  export interface AnnotationTrackLayout {
6
7
  index: number
@@ -11,6 +12,7 @@ export interface AnnotationTrackLayout {
11
12
  unit?: string
12
13
  lineType?: WaveformLineType
13
14
  points: WaveformPoint[]
15
+ source?: WaveformPointSource
14
16
  }
15
17
  left?: number
16
18
  top: number
@@ -9,7 +9,6 @@ import {
9
9
  ANNOTATION_AMBIGUITY_DISTANCE,
10
10
  ANNOTATION_HIT_RADIUS,
11
11
  findAnnotationSeriesCandidates,
12
- findNearestPointByX,
13
12
  } from './markup'
14
13
  import type {
15
14
  AnnotationEditorAnchor,
@@ -150,15 +149,15 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
150
149
  item.seriesList.some((series) => series.id === seriesId),
151
150
  )
152
151
  const series = track?.seriesList.find((item) => item.id === seriesId)
153
- const validPoints = series?.points.filter(
154
- (item) => Number.isFinite(item.x) && Number.isFinite(item.y),
155
- )
156
- const point = validPoints && draft ? findNearestPointByX(validPoints, draft.annotation.x) : null
157
- if (!draft || !candidate || !track || !validPoints?.length) {
152
+ const source = series?.source
153
+ const first = source?.pointAt(0)
154
+ const last = source?.pointAt((source?.length ?? 0) - 1)
155
+ const point = draft ? source?.nearestPoint(draft.annotation.x) : undefined
156
+ if (!draft || !candidate || !track || !first || !last || !point) {
158
157
  timeError.value = '当前波形没有有效数据'
159
158
  return
160
159
  }
161
- if (draft.annotation.x < validPoints[0].x || draft.annotation.x > validPoints.at(-1)!.x) {
160
+ if (draft.annotation.x < first.x || draft.annotation.x > last.x) {
162
161
  timeError.value = '时间超出当前波形范围'
163
162
  return
164
163
  }
@@ -183,20 +182,18 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
183
182
  item.seriesList.some((series) => series.id === draft.annotation.seriesId),
184
183
  )
185
184
  const series = track?.seriesList.find((item) => item.id === draft.annotation.seriesId)
186
- const validPoints = series?.points.filter(
187
- (point) => Number.isFinite(point.x) && Number.isFinite(point.y),
188
- )
189
- if (!validPoints?.length) {
185
+ const source = series?.source
186
+ const first = source?.pointAt(0)
187
+ const last = source?.pointAt((source?.length ?? 0) - 1)
188
+ if (!source || !first || !last) {
190
189
  timeError.value = '当前波形没有有效数据'
191
190
  return
192
191
  }
193
- const firstX = validPoints[0].x
194
- const lastX = validPoints[validPoints.length - 1].x
195
- if (rawTime < firstX || rawTime > lastX) {
192
+ if (rawTime < first.x || rawTime > last.x) {
196
193
  timeError.value = '时间超出当前波形范围'
197
194
  return
198
195
  }
199
- const point = findNearestPointByX(validPoints, rawTime)!
196
+ const point = source.nearestPoint(rawTime)!
200
197
  timeError.value = ''
201
198
  draft.annotation = { ...draft.annotation, x: point.x, y: point.y }
202
199
  }
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+
3
+ import { createLatestTaskScheduler } from './latestTaskScheduler'
4
+
5
+ function deferred() {
6
+ let resolve!: () => void
7
+ const promise = new Promise<void>((done) => {
8
+ resolve = done
9
+ })
10
+ return { promise, resolve }
11
+ }
12
+
13
+ describe('createLatestTaskScheduler', () => {
14
+ it('runs one task at a time and coalesces pending work to the latest task', async () => {
15
+ const scheduler = createLatestTaskScheduler()
16
+ const first = deferred()
17
+ const calls: number[] = []
18
+
19
+ scheduler.schedule(async (generation) => {
20
+ calls.push(generation)
21
+ await first.promise
22
+ })
23
+ scheduler.schedule(async (generation) => {
24
+ calls.push(generation)
25
+ })
26
+ const latest = scheduler.schedule(async (generation) => {
27
+ calls.push(generation)
28
+ })
29
+
30
+ expect(calls).toEqual([1])
31
+ expect(scheduler.isCurrent(1)).toBe(false)
32
+ expect(scheduler.isCurrent(latest)).toBe(true)
33
+ first.resolve()
34
+ await vi.waitFor(() => expect(calls).toEqual([1, 3]))
35
+ expect(scheduler.metrics).toEqual({ scheduled: 3, coalesced: 1, maxPending: 1 })
36
+ })
37
+
38
+ it('invalidates running work and discards pending work on cancellation or disposal', async () => {
39
+ const scheduler = createLatestTaskScheduler()
40
+ const first = deferred()
41
+ const firstGeneration = scheduler.schedule(async () => first.promise)
42
+ scheduler.schedule(async () => undefined)
43
+
44
+ scheduler.cancelPending()
45
+ expect(scheduler.isCurrent(firstGeneration)).toBe(false)
46
+ first.resolve()
47
+ await first.promise
48
+ scheduler.dispose()
49
+ expect(scheduler.schedule(async () => undefined)).toBe(0)
50
+ })
51
+ })
@@ -0,0 +1,75 @@
1
+ export interface LatestTaskSchedulerMetrics {
2
+ scheduled: number
3
+ coalesced: number
4
+ maxPending: number
5
+ }
6
+
7
+ export function resolveAutoSelectedMode(
8
+ visiblePointCount: number,
9
+ threshold: number,
10
+ hysteresis: number,
11
+ previousSelectedMode?: 'raw' | 'sampled',
12
+ ): 'raw' | 'sampled' {
13
+ if (!previousSelectedMode) return visiblePointCount <= threshold ? 'raw' : 'sampled'
14
+ return previousSelectedMode === 'raw'
15
+ ? visiblePointCount <= threshold + hysteresis
16
+ ? 'raw'
17
+ : 'sampled'
18
+ : visiblePointCount <= Math.max(0, threshold - hysteresis)
19
+ ? 'raw'
20
+ : 'sampled'
21
+ }
22
+
23
+ interface ScheduledTask {
24
+ generation: number
25
+ run: (generation: number) => Promise<void>
26
+ }
27
+
28
+ /** Runs at most one task at a time and retains only the newest pending task. */
29
+ export function createLatestTaskScheduler() {
30
+ let generation = 0
31
+ let running = false
32
+ let pending: ScheduledTask | undefined
33
+ let disposed = false
34
+ const metrics: LatestTaskSchedulerMetrics = { scheduled: 0, coalesced: 0, maxPending: 0 }
35
+
36
+ const drain = async () => {
37
+ if (running || disposed) return
38
+ running = true
39
+ try {
40
+ while (pending && !disposed) {
41
+ const task = pending
42
+ pending = undefined
43
+ await task.run(task.generation)
44
+ }
45
+ } finally {
46
+ running = false
47
+ }
48
+ }
49
+
50
+ return {
51
+ schedule(run: ScheduledTask['run']) {
52
+ if (disposed) return 0
53
+ generation += 1
54
+ metrics.scheduled += 1
55
+ if (pending) metrics.coalesced += 1
56
+ pending = { generation, run }
57
+ metrics.maxPending = Math.max(metrics.maxPending, 1)
58
+ void drain()
59
+ return generation
60
+ },
61
+ isCurrent(candidate: number) {
62
+ return !disposed && candidate === generation
63
+ },
64
+ cancelPending() {
65
+ generation += 1
66
+ pending = undefined
67
+ },
68
+ dispose() {
69
+ disposed = true
70
+ generation += 1
71
+ pending = undefined
72
+ },
73
+ metrics,
74
+ }
75
+ }
@@ -57,6 +57,8 @@ export interface BuildTrackLayoutsOptions {
57
57
  yAxisSplitNumber?: number
58
58
  yAxisNice?: boolean
59
59
  rendering: ResolvedWaveformRenderingOptions
60
+ /** Latest sampling result for SVG lines only; source series remain complete for interaction. */
61
+ linePointOverrides?: Readonly<Record<string, WaveformPoint[]>>
60
62
  hideSecondaryLabels: boolean
61
63
  yAxisLabelX: number
62
64
  showCompactEmptyTracks: boolean
@@ -200,6 +202,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
200
202
  options.rendering,
201
203
  {
202
204
  lineVisible: !isEmpty && trackSeries.lineType !== 'none',
205
+ linePointOverride: options.linePointOverrides?.[trackSeries.id],
203
206
  pointVisible: trackSeries.pointType !== 'none',
204
207
  errorBarVisible: trackSeries.errorBar.visible,
205
208
  hasErrorPoints: trackSeries.hasErrorPoints,
@@ -6,6 +6,7 @@ import type {
6
6
  WaveformPoint,
7
7
  WaveformPointType,
8
8
  } from '../../types'
9
+ import type { WaveformPointSource } from '../../core/waveformPointSource'
9
10
  import type { NormalizedWaveformGridLineOptions } from './grid'
10
11
 
11
12
  /**
@@ -23,6 +24,7 @@ export interface DisplaySeries {
23
24
  pointType: WaveformPointType
24
25
  errorBar: ResolvedWaveformErrorBarOptions
25
26
  points: WaveformPoint[]
27
+ source?: WaveformPointSource
26
28
  xDomain: [number, number]
27
29
  yDomain: [number, number]
28
30
  hasErrorPoints: boolean
@@ -26,6 +26,7 @@ import { useWaveformChartLifecycle } from './useWaveformChartLifecycle'
26
26
  import { usePreparedWaveformSeries } from './useWaveformData'
27
27
  import { useWaveformLayout } from './useWaveformLayout'
28
28
  import { useWaveformPresentation } from './useWaveformPresentation'
29
+ import { useWaveformRenderSampling } from './useWaveformRenderSampling'
29
30
  import type {
30
31
  ResolvedWaveformChartProps,
31
32
  ViewportSelectionState,
@@ -68,6 +69,7 @@ export function useWaveformChartController(
68
69
  const clipPathId = useWaveformInstanceId('waveform-clip')
69
70
  const internalHiddenSeriesIds = ref(new Set(props.defaultHiddenSeriesIds))
70
71
  const annotationInteraction = useWaveformAnnotationInteraction()
72
+ const linePointOverrides = shallowRef<Record<string, import('../../types').WaveformPoint[]>>({})
71
73
  const editorSeriesOptions = ref<AnnotationSeriesCandidate[]>([])
72
74
  const hoverThrottle = useAnimationFrameThrottle()
73
75
 
@@ -126,10 +128,12 @@ export function useWaveformChartController(
126
128
  sharedYDomains,
127
129
  independentYDomains,
128
130
  annotationInteraction: markRaw(annotationInteraction),
131
+ linePointOverrides,
129
132
  })
130
133
  const {
131
134
  chartSeries,
132
135
  chartTracks,
136
+ renderingOptions,
133
137
  trackLayouts,
134
138
  gridOptions,
135
139
  pageCount,
@@ -146,6 +150,16 @@ export function useWaveformChartController(
146
150
  resolveSeriesYScale,
147
151
  } = layout
148
152
 
153
+ useWaveformRenderSampling({
154
+ props,
155
+ emit,
156
+ instanceId: clipPathId,
157
+ preparedSeries,
158
+ sourceTrackLayouts: trackLayouts,
159
+ renderingOptions,
160
+ linePointOverrides,
161
+ })
162
+
149
163
  watchEffect(() => {
150
164
  paginationBandHeight.value =
151
165
  gridOptions.value.showPagination && pageCount.value > 1 && chartWidth.value <= 520 ? 40 : 0
@@ -0,0 +1,95 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { prepareWaveformSeries } from './useWaveformData'
4
+
5
+ describe('prepareWaveformSeries compact inputs', () => {
6
+ it('keeps dense typed samples as numeric columns until individual points are requested', () => {
7
+ const values = Float32Array.from({ length: 100_001 }, (_, index) => index / 10)
8
+ const snapshot = values.slice()
9
+ const [series] = prepareWaveformSeries({
10
+ kind: 'typed-samples',
11
+ values,
12
+ sampleRate: 1_000,
13
+ startTime: 2,
14
+ })
15
+
16
+ expect(series?.source.isCompact).toBe(true)
17
+ expect(series?.points).toHaveLength(values.length)
18
+ expect(series?.xDomain[0]).toBeLessThanOrEqual(2)
19
+ expect(series?.xDomain[1]).toBeGreaterThanOrEqual(102)
20
+ expect(series?.source.pointAt(50_000)).toEqual({ x: 52, y: 5_000 })
21
+ expect(values).toEqual(snapshot)
22
+
23
+ const workerDataset = series?.source.toWorkerDataset()
24
+ expect(workerDataset).toMatchObject({
25
+ kind: 'samples',
26
+ sampleRate: 1_000,
27
+ startTime: 2,
28
+ })
29
+ if (workerDataset?.kind === 'samples') {
30
+ expect(workerDataset).not.toHaveProperty('sourceIndexes')
31
+ expect(workerDataset.values.buffer).not.toBe(values.buffer)
32
+ expect(workerDataset.values).toEqual(values)
33
+ }
34
+ })
35
+
36
+ it('preserves invalid-sample time gaps without allocating an X column', () => {
37
+ const [series] = prepareWaveformSeries({
38
+ kind: 'typed-samples',
39
+ values: new Float64Array([1, Number.NaN, 3]),
40
+ sampleRate: 2,
41
+ startTime: 10,
42
+ })
43
+
44
+ expect(series?.source.pointsInRange()).toEqual([
45
+ { x: 10, y: 1 },
46
+ { x: 11, y: 3 },
47
+ ])
48
+ expect(series?.source.toWorkerDataset()).toMatchObject({
49
+ kind: 'samples',
50
+ values: new Float64Array([1, 3]),
51
+ sourceIndexes: new Uint32Array([0, 2]),
52
+ })
53
+ })
54
+
55
+ it('filters and stably orders compact typed points without materializing source point objects', () => {
56
+ const [series] = prepareWaveformSeries({
57
+ kind: 'typed-points',
58
+ x: new Float64Array([2, 1, 1, Number.NaN]),
59
+ y: new Float32Array([20, 10, 11, 12]),
60
+ error: new Float32Array([1, -1, Number.NaN, 4]),
61
+ })
62
+
63
+ expect(series?.source.isCompact).toBe(true)
64
+ expect(series?.source.pointsInRange()).toEqual([
65
+ { x: 1, y: 10 },
66
+ { x: 1, y: 11 },
67
+ { x: 2, y: 20, error: 1 },
68
+ ])
69
+ expect(series?.source.visibleRange([1, 1])).toEqual({ start: 0, end: 2 })
70
+ expect(series?.source.nearestPoint(-10)).toEqual({ x: 1, y: 10 })
71
+ expect(series?.source.nearestPoint(10)).toEqual({ x: 2, y: 20, error: 1 })
72
+ })
73
+
74
+ it('retains normal array access, iteration, slicing, mapping, and enumeration semantics', () => {
75
+ const [series] = prepareWaveformSeries({
76
+ kind: 'typed-samples',
77
+ values: new Float32Array([2, 4, 6]),
78
+ sampleRate: 2,
79
+ })
80
+
81
+ expect(series?.points[1]).toEqual({ x: 0.5, y: 4 })
82
+ expect(series?.points.at(-1)).toEqual({ x: 1, y: 6 })
83
+ expect([...(series?.points ?? [])]).toEqual([
84
+ { x: 0, y: 2 },
85
+ { x: 0.5, y: 4 },
86
+ { x: 1, y: 6 },
87
+ ])
88
+ expect(series?.points.slice(1)).toEqual([
89
+ { x: 0.5, y: 4 },
90
+ { x: 1, y: 6 },
91
+ ])
92
+ expect(series?.points.map((point) => point.y)).toEqual([2, 4, 6])
93
+ expect(Object.keys(series?.points ?? [])).toEqual(['0', '1', '2'])
94
+ })
95
+ })
@@ -1,6 +1,7 @@
1
1
  import { shallowRef, watch } from 'vue'
2
2
 
3
- import { normalizeWaveformSeries, resolveWaveformPointErrors } from '../../core'
3
+ import { normalizeWaveformSeriesSources } from '../../core/data'
4
+ import { lazyPointArray, type WaveformPointSource } from '../../core/waveformPointSource'
4
5
  import type {
5
6
  ResolvedWaveformErrorBarOptions,
6
7
  WaveformData,
@@ -22,43 +23,33 @@ export interface PreparedWaveformSeries {
22
23
  pointType: WaveformPointType
23
24
  errorBar: ResolvedWaveformErrorBarOptions
24
25
  points: WaveformPoint[]
26
+ source: WaveformPointSource
25
27
  xDomain: [number, number]
26
28
  yDomain: [number, number]
27
29
  hasErrorPoints: boolean
28
30
  }
29
31
 
30
- function pointMetrics(
31
- points: WaveformPoint[],
32
- includeErrors = false,
33
- ): { xDomain: [number, number]; yDomain: [number, number]; hasErrorPoints: boolean } {
34
- let xMinimum = Number.POSITIVE_INFINITY
35
- let xMaximum = Number.NEGATIVE_INFINITY
36
- let yMinimum = Number.POSITIVE_INFINITY
37
- let yMaximum = Number.NEGATIVE_INFINITY
38
- let hasErrorPoints = false
39
- for (const point of points) {
40
- if (point.x < xMinimum) xMinimum = point.x
41
- if (point.x > xMaximum) xMaximum = point.x
42
- if (point.y < yMinimum) yMinimum = point.y
43
- if (point.y > yMaximum) yMaximum = point.y
44
- if (includeErrors) {
45
- const errors = resolveWaveformPointErrors(point)
46
- if (errors.lower !== 0 || errors.upper !== 0) hasErrorPoints = true
47
- yMinimum = Math.min(yMinimum, point.y - errors.lower)
48
- yMaximum = Math.max(yMaximum, point.y + errors.upper)
49
- }
50
- }
51
- return {
52
- xDomain: paddedDomain(Number.isFinite(xMinimum) ? [xMinimum, xMaximum] : []),
53
- yDomain: paddedDomain(Number.isFinite(yMinimum) ? [yMinimum, yMaximum] : []),
54
- hasErrorPoints,
55
- }
56
- }
57
-
58
32
  export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSeries[] {
59
- return normalizeWaveformSeries(data).map((series) => {
60
- const metrics = pointMetrics(series.points, series.errorBar.visible)
61
- return { ...series, ...metrics }
33
+ return normalizeWaveformSeriesSources(data).map(({ source, ...series }) => {
34
+ const sourceMetrics = source.metrics(series.errorBar.visible)
35
+ return {
36
+ ...series,
37
+ source,
38
+ // Existing chart code remains array-oriented; compact sources materialize individual points
39
+ // only when an index is read.
40
+ points: lazyPointArray(source),
41
+ xDomain: paddedDomain(
42
+ Number.isFinite(sourceMetrics.xMinimum)
43
+ ? [sourceMetrics.xMinimum, sourceMetrics.xMaximum]
44
+ : [],
45
+ ),
46
+ yDomain: paddedDomain(
47
+ Number.isFinite(sourceMetrics.yMinimum)
48
+ ? [sourceMetrics.yMinimum, sourceMetrics.yMaximum]
49
+ : [],
50
+ ),
51
+ hasErrorPoints: sourceMetrics.hasErrorPoints,
52
+ }
62
53
  })
63
54
  }
64
55
 
@@ -1,6 +1,5 @@
1
1
  import { scaleLinear, type ZoomTransform } from 'd3'
2
2
  import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
3
-
4
3
  import { resolveWaveformRenderingOptions } from '../../core'
5
4
  import { paddedDomain } from '../../utils'
6
5
  import {
@@ -36,7 +35,6 @@ import type { PreparedWaveformSeries } from './useWaveformData'
36
35
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
37
36
  import { applyXDomainStrategy } from './xDomain'
38
37
  import type { useWaveformAnnotationInteraction } from '../annotation'
39
-
40
38
  interface LayoutContext {
41
39
  props: ResolvedWaveformChartProps
42
40
  preparedSeries: ShallowRef<PreparedWaveformSeries[]>
@@ -50,8 +48,8 @@ interface LayoutContext {
50
48
  sharedYDomains: Ref<Record<string, [number, number]>>
51
49
  independentYDomains: Ref<Record<number, [number, number]>>
52
50
  annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
51
+ linePointOverrides?: ShallowRef<Readonly<Record<string, import('../../types').WaveformPoint[]>>>
53
52
  }
54
-
55
53
  export function useWaveformLayout(context: LayoutContext) {
56
54
  const {
57
55
  props,
@@ -66,6 +64,7 @@ export function useWaveformLayout(context: LayoutContext) {
66
64
  sharedYDomains,
67
65
  independentYDomains,
68
66
  annotationInteraction,
67
+ linePointOverrides,
69
68
  } = context
70
69
  const chartSeries = computed<DisplaySeries[]>(() =>
71
70
  preparedSeries.value.map((series, index): DisplaySeries => ({
@@ -322,6 +321,7 @@ export function useWaveformLayout(context: LayoutContext) {
322
321
  yAxisSplitNumber: props.axes?.y?.splitNumber,
323
322
  yAxisNice: props.axes?.y?.nice,
324
323
  rendering: renderingOptions.value,
324
+ linePointOverrides: linePointOverrides?.value,
325
325
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
326
326
  yAxisLabelX: yAxisMetrics.value.labelCenterX,
327
327
  showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
@@ -370,10 +370,10 @@ export function useWaveformLayout(context: LayoutContext) {
370
370
  ? props.frameNumber + trackIndex
371
371
  : `${props.frameNumber}-${trackIndex + 1}`
372
372
  }
373
-
374
373
  return {
375
374
  chartSeries,
376
375
  chartTracks,
376
+ renderingOptions,
377
377
  gridOptions,
378
378
  pageCount,
379
379
  pagedTracks,
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { resolveAutoSelectedMode } from './latestTaskScheduler'
4
+
5
+ describe('resolveAutoSelectedMode', () => {
6
+ it('uses the exact threshold when no interaction selection is supplied', () => {
7
+ expect(resolveAutoSelectedMode(1_000, 1_000, 200)).toBe('raw')
8
+ expect(resolveAutoSelectedMode(1_001, 1_000, 200)).toBe('sampled')
9
+ })
10
+
11
+ it('keeps the previous mode inside the configured interaction hysteresis band', () => {
12
+ expect(resolveAutoSelectedMode(1_100, 1_000, 200, 'raw')).toBe('raw')
13
+ expect(resolveAutoSelectedMode(1_201, 1_000, 200, 'raw')).toBe('sampled')
14
+ expect(resolveAutoSelectedMode(900, 1_000, 200, 'sampled')).toBe('sampled')
15
+ expect(resolveAutoSelectedMode(800, 1_000, 200, 'sampled')).toBe('raw')
16
+ })
17
+ })