waveform-analysis 0.1.70 → 0.1.72
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.
- package/README.md +244 -45
- package/dist/assets/workerRuntime-5p-EI-GL.js +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.js +2924 -1915
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +24 -6
- package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
- package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
- package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
- package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
- package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
- package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
- package/dist/types/components/controls/useChartCommands.d.ts +42 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
- package/dist/types/components/controls/useControlMode.d.ts +8 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
- package/dist/types/components/core/layout.d.ts +5 -3
- package/dist/types/components/core/niceScale.d.ts +34 -0
- package/dist/types/components/core/pagination.d.ts +2 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
- package/dist/types/components/core/trackPagination.d.ts +1 -1
- package/dist/types/components/core/types.d.ts +3 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +24 -5
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
- package/dist/types/components/core/useWaveformData.d.ts +1 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
- package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
- package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
- package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
- package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisPadding.d.ts +7 -0
- package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
- package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
- package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +0 -1
- package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
- package/dist/types/components/rendering/compactFrame.d.ts +5 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
- package/dist/types/core/lineBoundary.d.ts +4 -0
- package/dist/types/core/rendering.d.ts +4 -0
- package/dist/types/core/renderingStrategies.d.ts +2 -0
- package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
- package/dist/types/data/simulatedWaveforms.d.ts +1 -1
- package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
- package/dist/types/demo/types.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types/chart.d.ts +30 -8
- package/dist/types/types/controls.d.ts +69 -0
- package/dist/types/types/index.d.ts +2 -1
- package/dist/types/utils/formatters.d.ts +1 -0
- package/package.json +7 -6
- package/src/App.vue +58 -14
- package/src/components/WaveformChart.css +10 -1
- package/src/components/WaveformChart.vue +6 -0
- package/src/components/WaveformChartView.vue +27 -23
- package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
- package/src/components/annotation/useWaveformChartAnnotations.ts +6 -1
- package/src/components/controls/WaveformToolbar.vue +248 -0
- package/src/components/controls/chartExportOptions.ts +32 -0
- package/src/components/controls/chartSnapshot.ts +132 -0
- package/src/components/controls/snapshotTitle.ts +73 -0
- package/src/components/controls/toolbarRegistry.ts +92 -0
- package/src/components/controls/useChartCommands.ts +294 -0
- package/src/components/controls/useChartImageExport.ts +155 -0
- package/src/components/controls/useControlMode.ts +30 -0
- package/src/components/core/latestTaskScheduler.ts +14 -2
- package/src/components/core/layout.ts +35 -34
- package/src/components/core/niceScale.ts +151 -0
- package/src/components/core/pagination.ts +3 -16
- package/src/components/core/trackLayoutBuilder.ts +34 -17
- package/src/components/core/trackPagination.ts +3 -3
- package/src/components/core/types.ts +3 -0
- package/src/components/core/useWaveformChartController.ts +95 -10
- package/src/components/core/useWaveformChartLifecycle.ts +11 -111
- package/src/components/core/useWaveformData.ts +2 -0
- package/src/components/core/useWaveformLayout.ts +17 -5
- package/src/components/core/useWaveformMeasurements.ts +63 -0
- package/src/components/core/useWaveformRenderSampling.ts +80 -59
- package/src/components/core/useWaveformViewportRestore.ts +105 -0
- package/src/components/core/useWaveformYAxisLayout.ts +3 -2
- package/src/components/core/waveformChartTypes.ts +5 -0
- package/src/components/core/waveformSamplingSession.ts +49 -0
- package/src/components/core/workerSamplingClient.ts +38 -6
- package/src/components/core/yAxisLabelStyle.ts +10 -0
- package/src/components/core/yAxisLayoutMetrics.ts +10 -9
- package/src/components/core/yAxisPadding.ts +45 -0
- package/src/components/core/yAxisTitleLayout.ts +11 -12
- package/src/components/interaction/useWaveformViewport.ts +11 -2
- package/src/components/rendering/WaveformTrack.css +1 -2
- package/src/components/rendering/WaveformTrack.vue +4 -12
- package/src/components/rendering/WaveformTrackAxes.vue +48 -19
- package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
- package/src/components/rendering/compactFrame.ts +31 -0
- package/src/components/rendering/yAxisTickVisibility.ts +95 -0
- package/src/core/lineBoundary.ts +24 -0
- package/src/core/rendering.ts +48 -25
- package/src/core/renderingStrategies.ts +19 -13
- package/src/core/waveformPointSource.ts +23 -1
- package/src/core/workerSampling/outputBoundaries.ts +32 -0
- package/src/core/workerSampling/repository.ts +26 -18
- package/src/core/workerSampling/repositoryDataset.ts +33 -1
- package/src/core/workerSampling/workerRuntime.ts +13 -3
- package/src/data/simulatedWaveforms.ts +57 -2
- package/src/demo/DemoChartHost.vue +5 -1
- package/src/demo/DemoGridFrameControls.vue +2 -0
- package/src/demo/DemoViewControls.vue +5 -2
- package/src/demo/DemoYAxisPaddingControls.vue +46 -0
- package/src/demo/embedBridge.ts +85 -0
- package/src/demo/types.ts +6 -0
- package/src/index.ts +18 -0
- package/src/types/chart.ts +32 -8
- package/src/types/controls.ts +80 -0
- package/src/types/index.ts +18 -0
- package/src/utils/formatters.ts +2 -1
- package/src/views/WasmSamplingDemo.vue +8 -1
- package/dist/assets/workerRuntime-BcYng9TR.js +0 -1
package/src/core/rendering.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { pointSourceFromPoints, type WaveformPointSource } from './waveformPointSource'
|
|
2
|
+
import { withLineBoundaries } from './lineBoundary'
|
|
1
3
|
import { bisector } from 'd3'
|
|
2
4
|
|
|
3
5
|
import type { WaveformPoint } from '../types'
|
|
@@ -20,6 +22,7 @@ const acceptAllPoints = () => true
|
|
|
20
22
|
|
|
21
23
|
interface PointSeriesSource {
|
|
22
24
|
points: WaveformPoint[]
|
|
25
|
+
source?: WaveformPointSource
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
interface SeriesRenderSelectionOptions {
|
|
@@ -58,9 +61,10 @@ export function hasMinimumVisibleXValues(
|
|
|
58
61
|
const required = Math.ceil(minimum)
|
|
59
62
|
const xValues = new Set<number>()
|
|
60
63
|
for (const series of seriesList) {
|
|
61
|
-
const
|
|
64
|
+
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
65
|
+
const range = source.visibleRange(domain)
|
|
62
66
|
for (let index = range.start; index < range.end; index += 1) {
|
|
63
|
-
xValues.add(
|
|
67
|
+
xValues.add(source.pointAt(index)!.x)
|
|
64
68
|
if (xValues.size >= required) return true
|
|
65
69
|
}
|
|
66
70
|
}
|
|
@@ -68,18 +72,20 @@ export function hasMinimumVisibleXValues(
|
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
function selectRenderablePointsInRange(
|
|
71
|
-
points:
|
|
75
|
+
points: WaveformPointSource,
|
|
72
76
|
range: VisiblePointRange,
|
|
73
77
|
domain: [number, number],
|
|
74
78
|
width: number,
|
|
75
79
|
options: ResolvedWaveformRenderingOptions,
|
|
76
80
|
): WaveformPoint[] {
|
|
81
|
+
if (range.start === range.end && (range.start === 0 || range.end === points.length)) return []
|
|
77
82
|
const start = Math.max(0, range.start - 1)
|
|
78
83
|
const end = Math.min(points.length, range.end + 1)
|
|
79
84
|
const visibleCount = end - start
|
|
80
85
|
if (visibleCount <= 0) return []
|
|
81
86
|
return resolveRenderablePointSelectionStrategy({ visibleCount, width, options })({
|
|
82
|
-
points,
|
|
87
|
+
points: [],
|
|
88
|
+
source: points,
|
|
83
89
|
range,
|
|
84
90
|
domain,
|
|
85
91
|
width,
|
|
@@ -96,25 +102,25 @@ function shouldDeferLineSampling(
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
function selectSamplingPlaceholderPoints(
|
|
99
|
-
points:
|
|
105
|
+
points: WaveformPointSource,
|
|
100
106
|
range: VisiblePointRange,
|
|
101
107
|
width: number,
|
|
102
108
|
options: ResolvedWaveformRenderingOptions,
|
|
103
109
|
): WaveformPoint[] {
|
|
104
|
-
const start =
|
|
105
|
-
const end = Math.min(points.length, range.end + 1)
|
|
110
|
+
const { start, end } = range
|
|
106
111
|
const count = end - start
|
|
107
|
-
if (count <= 0) return []
|
|
108
|
-
if (options.sampling.strategy === 'none') return points.slice(start, end)
|
|
109
112
|
const target =
|
|
110
113
|
options.sampling.maxPointCount ??
|
|
111
114
|
Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
const interior =
|
|
116
|
+
count <= target || options.sampling.strategy === 'none'
|
|
117
|
+
? points.pointsInRange(start, end)
|
|
118
|
+
: Array.from({ length: target }, (_, index) =>
|
|
119
|
+
points.pointAt(
|
|
120
|
+
start + (target === 1 ? 0 : Math.round((index * (count - 1)) / (target - 1))),
|
|
121
|
+
)!,
|
|
122
|
+
)
|
|
123
|
+
return withLineBoundaries(interior, range, points.length, (index) => points.pointAt(index)!)
|
|
118
124
|
}
|
|
119
125
|
|
|
120
126
|
/**
|
|
@@ -129,7 +135,7 @@ export function selectRenderablePoints(
|
|
|
129
135
|
): WaveformPoint[] {
|
|
130
136
|
if (!points.length || width <= 0) return []
|
|
131
137
|
return selectRenderablePointsInRange(
|
|
132
|
-
points,
|
|
138
|
+
pointSourceFromPoints(points),
|
|
133
139
|
resolveVisiblePointRange(points, domain),
|
|
134
140
|
domain,
|
|
135
141
|
width,
|
|
@@ -138,7 +144,7 @@ export function selectRenderablePoints(
|
|
|
138
144
|
}
|
|
139
145
|
|
|
140
146
|
function selectDecorationPointsInRange(
|
|
141
|
-
points:
|
|
147
|
+
points: WaveformPointSource,
|
|
142
148
|
range: VisiblePointRange,
|
|
143
149
|
domain: [number, number],
|
|
144
150
|
width: number,
|
|
@@ -148,10 +154,10 @@ function selectDecorationPointsInRange(
|
|
|
148
154
|
priorityPredicate?: (point: WaveformPoint) => boolean,
|
|
149
155
|
): WaveformPoint[] {
|
|
150
156
|
if (!downsample || minSpacing === 0) {
|
|
151
|
-
if (predicate === acceptAllPoints) return points.
|
|
157
|
+
if (predicate === acceptAllPoints) return points.pointsInRange(range.start, range.end)
|
|
152
158
|
const visiblePoints: WaveformPoint[] = []
|
|
153
159
|
for (let index = range.start; index < range.end; index += 1) {
|
|
154
|
-
if (predicate(points
|
|
160
|
+
if (predicate(points.pointAt(index)!)) visiblePoints.push(points.pointAt(index)!)
|
|
155
161
|
}
|
|
156
162
|
return visiblePoints
|
|
157
163
|
}
|
|
@@ -161,7 +167,7 @@ function selectDecorationPointsInRange(
|
|
|
161
167
|
const span = domainEnd - domainStart
|
|
162
168
|
if (span <= 0) {
|
|
163
169
|
for (let index = range.start; index < range.end; index += 1) {
|
|
164
|
-
if (predicate(points
|
|
170
|
+
if (predicate(points.pointAt(index)!)) return [points.pointAt(index)!]
|
|
165
171
|
}
|
|
166
172
|
return []
|
|
167
173
|
}
|
|
@@ -208,7 +214,7 @@ function selectDecorationPointsInRange(
|
|
|
208
214
|
}
|
|
209
215
|
|
|
210
216
|
for (let index = range.start; index < range.end; index += 1) {
|
|
211
|
-
const point = points
|
|
217
|
+
const point = points.pointAt(index)!
|
|
212
218
|
if (!predicate(point)) continue
|
|
213
219
|
first ??= point
|
|
214
220
|
last = point
|
|
@@ -252,7 +258,7 @@ export function selectDecorationPoints(
|
|
|
252
258
|
): WaveformPoint[] {
|
|
253
259
|
if (!points.length || width <= 0) return []
|
|
254
260
|
return selectDecorationPointsInRange(
|
|
255
|
-
points,
|
|
261
|
+
pointSourceFromPoints(points),
|
|
256
262
|
resolveVisiblePointRange(points, domain),
|
|
257
263
|
domain,
|
|
258
264
|
width,
|
|
@@ -268,8 +274,8 @@ function hasPointError(point: WaveformPoint): boolean {
|
|
|
268
274
|
return lower !== 0 || upper !== 0
|
|
269
275
|
}
|
|
270
276
|
|
|
271
|
-
export function
|
|
272
|
-
points:
|
|
277
|
+
export function selectSeriesRenderSourcePoints(
|
|
278
|
+
points: WaveformPointSource,
|
|
273
279
|
domain: [number, number],
|
|
274
280
|
width: number,
|
|
275
281
|
rendering: ResolvedWaveformRenderingOptions,
|
|
@@ -278,7 +284,7 @@ export function selectSeriesRenderPoints(
|
|
|
278
284
|
if (!points.length || width <= 0) {
|
|
279
285
|
return { linePoints: [], pointRenderPoints: [], errorBarRenderPoints: [] }
|
|
280
286
|
}
|
|
281
|
-
const range =
|
|
287
|
+
const range = points.visibleRange(domain)
|
|
282
288
|
const linePoints = selection.lineVisible
|
|
283
289
|
? (selection.linePointOverride ??
|
|
284
290
|
(shouldDeferLineSampling(range.end - range.start, rendering)
|
|
@@ -329,3 +335,20 @@ export function selectSeriesRenderPoints(
|
|
|
329
335
|
: [],
|
|
330
336
|
}
|
|
331
337
|
}
|
|
338
|
+
|
|
339
|
+
/** Public array adapter; internal layout passes its normalized source directly. */
|
|
340
|
+
export function selectSeriesRenderPoints(
|
|
341
|
+
points: WaveformPoint[],
|
|
342
|
+
domain: [number, number],
|
|
343
|
+
width: number,
|
|
344
|
+
rendering: ResolvedWaveformRenderingOptions,
|
|
345
|
+
selection: SeriesRenderSelectionOptions,
|
|
346
|
+
): SeriesRenderPointSelection {
|
|
347
|
+
return selectSeriesRenderSourcePoints(
|
|
348
|
+
pointSourceFromPoints(points),
|
|
349
|
+
domain,
|
|
350
|
+
width,
|
|
351
|
+
rendering,
|
|
352
|
+
selection,
|
|
353
|
+
)
|
|
354
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { pointSourceFromPoints, type WaveformPointSource } from './waveformPointSource'
|
|
1
2
|
import type { WaveformPoint } from '../types'
|
|
2
3
|
import type { ResolvedWaveformRenderingOptions } from './renderingOptions'
|
|
3
4
|
|
|
@@ -7,6 +8,7 @@ export interface VisiblePointRange {
|
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface RenderablePointSelectionContext {
|
|
11
|
+
source?: WaveformPointSource
|
|
10
12
|
points: WaveformPoint[]
|
|
11
13
|
range: VisiblePointRange
|
|
12
14
|
domain: [number, number]
|
|
@@ -25,17 +27,15 @@ function selectionBounds(range: VisiblePointRange, pointCount: number) {
|
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
29
|
|
|
28
|
-
function pushUniquePoint(target: WaveformPoint[], point: WaveformPoint | undefined) {
|
|
29
|
-
if (point && target[target.length - 1] !== point) target.push(point)
|
|
30
|
-
}
|
|
31
|
-
|
|
32
30
|
export const completePointSelectionStrategy: RenderablePointSelectionStrategy = (context) => {
|
|
33
|
-
const
|
|
34
|
-
|
|
31
|
+
const source = context.source ?? pointSourceFromPoints(context.points)
|
|
32
|
+
const { start, end } = selectionBounds(context.range, source.length)
|
|
33
|
+
return source.pointsInRange(start, end)
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrategy = (context) => {
|
|
38
|
-
const {
|
|
37
|
+
const { range, domain, width, options } = context
|
|
38
|
+
const points = context.source ?? pointSourceFromPoints(context.points)
|
|
39
39
|
const { start, end } = selectionBounds(range, points.length)
|
|
40
40
|
const visibleCount = end - start
|
|
41
41
|
if (visibleCount <= 0) return []
|
|
@@ -48,6 +48,12 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
|
|
|
48
48
|
)
|
|
49
49
|
const bucketCount = Math.max(1, Math.floor(maximumPointCount / 4))
|
|
50
50
|
const result: WaveformPoint[] = []
|
|
51
|
+
let lastSelectedIndex = -1
|
|
52
|
+
const pushIndex = (index: number) => {
|
|
53
|
+
if (index < 0 || index >= points.length || index === lastSelectedIndex) return
|
|
54
|
+
result.push(points.pointAt(index)!)
|
|
55
|
+
lastSelectedIndex = index
|
|
56
|
+
}
|
|
51
57
|
const span = domainEnd - domainStart || 1
|
|
52
58
|
const bucketIndexes = Array.from({ length: 4 }, () => -1)
|
|
53
59
|
let activeBucket = -1
|
|
@@ -82,13 +88,13 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
|
|
|
82
88
|
bucketIndexes[position + 1] = value
|
|
83
89
|
}
|
|
84
90
|
for (let index = 0; index < count; index += 1) {
|
|
85
|
-
|
|
91
|
+
pushIndex(bucketIndexes[index])
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
89
|
-
|
|
95
|
+
pushIndex(start)
|
|
90
96
|
for (let index = range.start; index < range.end; index += 1) {
|
|
91
|
-
const point = points
|
|
97
|
+
const point = points.pointAt(index)!
|
|
92
98
|
const bucket = Math.min(
|
|
93
99
|
bucketCount - 1,
|
|
94
100
|
Math.max(0, Math.floor(((point.x - domainStart) / span) * bucketCount)),
|
|
@@ -103,11 +109,11 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
|
|
|
103
109
|
continue
|
|
104
110
|
}
|
|
105
111
|
lastIndex = index
|
|
106
|
-
if (point.y < points
|
|
107
|
-
if (point.y > points
|
|
112
|
+
if (point.y < points.pointAt(minimumIndex)!.y) minimumIndex = index
|
|
113
|
+
if (point.y > points.pointAt(maximumIndex)!.y) maximumIndex = index
|
|
108
114
|
}
|
|
109
115
|
flushBucket()
|
|
110
|
-
|
|
116
|
+
pushIndex(end - 1)
|
|
111
117
|
return result
|
|
112
118
|
}
|
|
113
119
|
|
|
@@ -288,6 +288,26 @@ function compactSamples(data: Extract<SingleWaveformData, { kind: 'typed-samples
|
|
|
288
288
|
function compactPoints(data: Extract<SingleWaveformData, { kind: 'typed-points' }>) {
|
|
289
289
|
if (!typedPointsAreValid(data))
|
|
290
290
|
return new CompactWaveformPointSource(new Float64Array(), new Float64Array())
|
|
291
|
+
// Validate once before the common dense/sorted path; always take private copies.
|
|
292
|
+
let denseOrdered = true
|
|
293
|
+
for (let index = 0; index < data.x.length; index += 1) {
|
|
294
|
+
if (
|
|
295
|
+
!Number.isFinite(data.x[index]) ||
|
|
296
|
+
!Number.isFinite(data.y[index]) ||
|
|
297
|
+
(index > 0 && data.x[index]! < data.x[index - 1]!)
|
|
298
|
+
) {
|
|
299
|
+
denseOrdered = false
|
|
300
|
+
break
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (denseOrdered)
|
|
304
|
+
return new CompactWaveformPointSource(
|
|
305
|
+
data.x.slice(),
|
|
306
|
+
data.y.slice(),
|
|
307
|
+
data.error?.slice(),
|
|
308
|
+
data.lowerError?.slice(),
|
|
309
|
+
data.upperError?.slice(),
|
|
310
|
+
)
|
|
291
311
|
const indexes: number[] = []
|
|
292
312
|
let ordered = true
|
|
293
313
|
let previousX = Number.NEGATIVE_INFINITY
|
|
@@ -356,11 +376,13 @@ export function lazyPointArray(source: WaveformPointSource): WaveformPoint[] {
|
|
|
356
376
|
}
|
|
357
377
|
return point
|
|
358
378
|
}
|
|
359
|
-
|
|
379
|
+
// Keep the compatibility facade sparse: no point-sized backing array is allocated for compact data.
|
|
380
|
+
return new Proxy<WaveformPoint[]>([], {
|
|
360
381
|
get(target, property, receiver) {
|
|
361
382
|
if (typeof property === 'string' && /^(0|[1-9]\d*)$/.test(property)) {
|
|
362
383
|
return valueAt(Number(property))
|
|
363
384
|
}
|
|
385
|
+
if (property === 'length') return source.length
|
|
364
386
|
return Reflect.get(target, property, receiver)
|
|
365
387
|
},
|
|
366
388
|
has(target, property) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WaveformPointRange } from '../waveformPointSource'
|
|
2
|
+
import type { WorkerSamplingOutput } from './protocol'
|
|
3
|
+
import { datasetLength, datasetXAt, type StoredDataset } from './repositoryDataset'
|
|
4
|
+
|
|
5
|
+
export function addOutputBoundaries(
|
|
6
|
+
output: WorkerSamplingOutput,
|
|
7
|
+
range: WaveformPointRange,
|
|
8
|
+
dataset: StoredDataset,
|
|
9
|
+
): WorkerSamplingOutput {
|
|
10
|
+
const length = datasetLength(dataset)
|
|
11
|
+
if (range.start === range.end && (range.start === 0 || range.end === length)) return output
|
|
12
|
+
const prefix = range.start > 0 ? [range.start - 1] : []
|
|
13
|
+
const suffix = range.end < length ? [range.end] : []
|
|
14
|
+
if (output.kind === 'source-indexes') {
|
|
15
|
+
const values = Array.from(output.sourceIndexes)
|
|
16
|
+
return {
|
|
17
|
+
kind: 'source-indexes',
|
|
18
|
+
sourceIndexes: Uint32Array.from([...prefix, ...values, ...suffix]),
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const x = Array.from(output.x)
|
|
22
|
+
const y = Array.from(output.y)
|
|
23
|
+
if (prefix.length) {
|
|
24
|
+
x.unshift(datasetXAt(dataset, prefix[0]!))
|
|
25
|
+
y.unshift(dataset.y[prefix[0]!]!)
|
|
26
|
+
}
|
|
27
|
+
if (suffix.length) {
|
|
28
|
+
x.push(datasetXAt(dataset, suffix[0]!))
|
|
29
|
+
y.push(dataset.y[suffix[0]!]!)
|
|
30
|
+
}
|
|
31
|
+
return { kind: 'aggregates', x: Float64Array.from(x), y: Float64Array.from(y) }
|
|
32
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { addOutputBoundaries } from './outputBoundaries'
|
|
1
2
|
import type { WaveformPoint } from '../../types'
|
|
2
3
|
import type { WorkerSamplingDataset } from '../waveformPointSource'
|
|
3
4
|
import { javascriptSamplingBackend } from './javascriptBackend'
|
|
@@ -194,13 +195,17 @@ export class WorkerSamplingRepository {
|
|
|
194
195
|
requestId,
|
|
195
196
|
request,
|
|
196
197
|
'ok',
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
198
|
+
addOutputBoundaries(
|
|
199
|
+
{
|
|
200
|
+
kind: 'source-indexes',
|
|
201
|
+
sourceIndexes: Uint32Array.from(
|
|
202
|
+
{ length: visiblePointCount },
|
|
203
|
+
(_, index) => range.start + index,
|
|
204
|
+
),
|
|
205
|
+
},
|
|
206
|
+
range,
|
|
207
|
+
dataset,
|
|
208
|
+
),
|
|
204
209
|
'raw',
|
|
205
210
|
'raw',
|
|
206
211
|
visiblePointCount,
|
|
@@ -256,18 +261,21 @@ export class WorkerSamplingRepository {
|
|
|
256
261
|
)
|
|
257
262
|
: undefined
|
|
258
263
|
: dataset.index?.sample(range.start, range.end, strategy, target)
|
|
259
|
-
const output =
|
|
264
|
+
const output = addOutputBoundaries(
|
|
260
265
|
indexed ??
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
266
|
+
outputWithGlobalIndexes(
|
|
267
|
+
backend.sample({
|
|
268
|
+
x: datasetXRange(dataset, range.start, range.end),
|
|
269
|
+
y: dataset.y.subarray(range.start, range.end),
|
|
270
|
+
points: dataset.points?.slice(range.start, range.end) ?? [],
|
|
271
|
+
strategy,
|
|
272
|
+
targetPointCount: target,
|
|
273
|
+
}),
|
|
274
|
+
range.start,
|
|
275
|
+
),
|
|
276
|
+
range,
|
|
277
|
+
dataset,
|
|
278
|
+
)
|
|
271
279
|
this.cache.set(key, output)
|
|
272
280
|
if (dataset.wasmDatasetHandle !== undefined && isWasmDatasetBackend(backend)) {
|
|
273
281
|
dataset.wasmIndexBytes = backend.datasetIndexBytes(dataset.wasmDatasetHandle)
|
|
@@ -69,8 +69,16 @@ export function isWasmDatasetBackend(
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function copyFiniteNumericValues(sourceX: Float64Array, sourceY: Float32Array | Float64Array) {
|
|
72
|
-
const indexes: number[] = []
|
|
73
72
|
const length = Math.min(sourceX.length, sourceY.length)
|
|
73
|
+
let denseOrdered = sourceX.length === sourceY.length
|
|
74
|
+
for (let index = 0; index < length && denseOrdered; index += 1) {
|
|
75
|
+
denseOrdered =
|
|
76
|
+
Number.isFinite(sourceX[index]) &&
|
|
77
|
+
Number.isFinite(sourceY[index]) &&
|
|
78
|
+
(index === 0 || sourceX[index]! >= sourceX[index - 1]!)
|
|
79
|
+
}
|
|
80
|
+
if (denseOrdered) return { x: sourceX.slice(), y: Float64Array.from(sourceY) }
|
|
81
|
+
const indexes: number[] = []
|
|
74
82
|
for (let index = 0; index < length; index += 1) {
|
|
75
83
|
if (Number.isFinite(sourceX[index]) && Number.isFinite(sourceY[index])) indexes.push(index)
|
|
76
84
|
}
|
|
@@ -120,6 +128,30 @@ export function createStoredDataset(
|
|
|
120
128
|
},
|
|
121
129
|
}
|
|
122
130
|
}
|
|
131
|
+
// Compact, already-valid sample messages need neither an index array nor a sort.
|
|
132
|
+
if (!numeric.sourceIndexes && numeric.values.every(Number.isFinite)) {
|
|
133
|
+
const y = Float64Array.from(numeric.values)
|
|
134
|
+
let min = Infinity
|
|
135
|
+
let max = -Infinity
|
|
136
|
+
for (const value of y) {
|
|
137
|
+
min = Math.min(min, value)
|
|
138
|
+
max = Math.max(max, value)
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
y,
|
|
142
|
+
sampleStartTime: numeric.startTime,
|
|
143
|
+
sampleRate: numeric.sampleRate,
|
|
144
|
+
revision,
|
|
145
|
+
metrics: {
|
|
146
|
+
inputPointCount: y.length,
|
|
147
|
+
validPointCount: y.length,
|
|
148
|
+
xDomain: y.length
|
|
149
|
+
? [numeric.startTime, numeric.startTime + (y.length - 1) / numeric.sampleRate]
|
|
150
|
+
: null,
|
|
151
|
+
yDomain: y.length ? [min, max] : null,
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
}
|
|
123
155
|
const indexes = Array.from({ length: numeric.values.length }, (_, index) => index).filter(
|
|
124
156
|
(index) => Number.isFinite(numeric.values[index]),
|
|
125
157
|
)
|
|
@@ -31,10 +31,20 @@ function addWasmError(response: WorkerSamplingResponse): WorkerSamplingResponse
|
|
|
31
31
|
|
|
32
32
|
const workerScope = globalThis as typeof globalThis & {
|
|
33
33
|
onmessage: ((event: MessageEvent<WorkerSamplingRequest>) => void) | null
|
|
34
|
-
postMessage: (
|
|
34
|
+
postMessage: (
|
|
35
|
+
message: WorkerSamplingResponse | { type: 'request-error'; requestId: number; message: string },
|
|
36
|
+
) => void
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
workerScope.onmessage = async (event: MessageEvent<WorkerSamplingRequest>) => {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
try {
|
|
41
|
+
if (requiresWasm(event.data)) await initializeWasmBackend()
|
|
42
|
+
workerScope.postMessage(addWasmError(repository.handle(event.data)))
|
|
43
|
+
} catch (error) {
|
|
44
|
+
workerScope.postMessage({
|
|
45
|
+
type: 'request-error',
|
|
46
|
+
requestId: event.data.requestId,
|
|
47
|
+
message: error instanceof Error ? error.message : String(error),
|
|
48
|
+
})
|
|
49
|
+
}
|
|
40
50
|
}
|
|
@@ -140,14 +140,69 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
140
140
|
signal: (time, noise) => 0.7 * Math.sin(TWO_PI * 0.45 * time) + noise * 0.36,
|
|
141
141
|
errors: () => ({ error: 0.12 }),
|
|
142
142
|
},
|
|
143
|
+
{
|
|
144
|
+
id: 'simulated-mega-voltage',
|
|
145
|
+
shotNo: '13300',
|
|
146
|
+
name: '百万量级电压',
|
|
147
|
+
unit: 'V',
|
|
148
|
+
lineType: 'linear',
|
|
149
|
+
pointType: 'none',
|
|
150
|
+
signal: (time) => 2.4e6 * Math.sin(TWO_PI * 0.6 * time),
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'simulated-micro-current',
|
|
154
|
+
shotNo: '13300',
|
|
155
|
+
name: '微量级电流',
|
|
156
|
+
unit: 'A',
|
|
157
|
+
lineType: 'linear',
|
|
158
|
+
pointType: 'none',
|
|
159
|
+
signal: (time, noise) => 3.2e-6 * Math.sin(TWO_PI * 0.45 * time) + noise * 2e-7,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
id: 'simulated-giga-pressure',
|
|
163
|
+
shotNo: '13300',
|
|
164
|
+
name: '十亿量级压力脉冲',
|
|
165
|
+
unit: 'Pa',
|
|
166
|
+
lineType: 'linear',
|
|
167
|
+
pointType: 'none',
|
|
168
|
+
signal: (time) => 1.8e9 + 4e8 * Math.exp(-((time - 0.75) ** 2) / 0.4),
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: 'simulated-nano-displacement',
|
|
172
|
+
shotNo: '13300',
|
|
173
|
+
name: '纳量级阻尼位移',
|
|
174
|
+
unit: 'm',
|
|
175
|
+
lineType: 'linear',
|
|
176
|
+
pointType: 'none',
|
|
177
|
+
signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
|
|
178
|
+
},
|
|
143
179
|
]
|
|
144
180
|
|
|
145
|
-
export function createSimulatedWaveformData(): WaveformData {
|
|
181
|
+
export function createSimulatedWaveformData(trackCount?: number): WaveformData {
|
|
146
182
|
const noise = createSeededNoise(0x5eed1234)
|
|
147
183
|
const errorNoise = createSeededNoise(0xe220a11)
|
|
184
|
+
const definitions = [...seriesDefinitions]
|
|
185
|
+
const existingTrackCount = new Set(definitions.map((series) => series.trackId ?? series.id)).size
|
|
186
|
+
const extraTrackCount =
|
|
187
|
+
trackCount !== undefined && Number.isFinite(trackCount)
|
|
188
|
+
? Math.max(0, Math.min(100, Math.floor(trackCount)) - existingTrackCount)
|
|
189
|
+
: 0
|
|
190
|
+
for (let index = 0; index < extraTrackCount; index += 1) {
|
|
191
|
+
const channel = existingTrackCount + index + 1
|
|
192
|
+
definitions.push({
|
|
193
|
+
id: `simulated-extra-${channel}`,
|
|
194
|
+
name: `模拟通道 ${channel}`,
|
|
195
|
+
unit: 'V',
|
|
196
|
+
lineType: 'linear',
|
|
197
|
+
pointType: 'none',
|
|
198
|
+
signal: (time) =>
|
|
199
|
+
(1 + index * 0.3) * Math.sin(TWO_PI * (0.2 + index * 0.15) * time + index) +
|
|
200
|
+
0.2 * Math.cos(TWO_PI * 1.4 * time),
|
|
201
|
+
})
|
|
202
|
+
}
|
|
148
203
|
return {
|
|
149
204
|
kind: 'series',
|
|
150
|
-
series:
|
|
205
|
+
series: definitions.map(({ signal, errors, minimumX, ...series }) => {
|
|
151
206
|
const points = createPoints(signal, noise, errors, errorNoise)
|
|
152
207
|
return {
|
|
153
208
|
...series,
|
|
@@ -19,6 +19,8 @@ defineExpose({ resetViewport })
|
|
|
19
19
|
<section class="chart-panel">
|
|
20
20
|
<WaveformChart
|
|
21
21
|
ref="waveformChartRef"
|
|
22
|
+
layout-preset="edge-compact"
|
|
23
|
+
unit-display-mode="channel-label"
|
|
22
24
|
v-model:annotations="model.annotations"
|
|
23
25
|
v-model:hidden-series-ids="model.hiddenSeriesIds"
|
|
24
26
|
:data="model.data"
|
|
@@ -50,7 +52,9 @@ defineExpose({ resetViewport })
|
|
|
50
52
|
:zero-line="model.zeroLine"
|
|
51
53
|
:frame-number="model.frameWatermarkVisible ? 1 : undefined"
|
|
52
54
|
:annotations-visible="model.annotationsVisible"
|
|
53
|
-
:interaction-mode="model.interactionMode"
|
|
55
|
+
v-model:interaction-mode="model.interactionMode"
|
|
56
|
+
:toolbar="model.toolbarVisible"
|
|
57
|
+
pannable
|
|
54
58
|
@zoom-end="model.handleZoomEnd"
|
|
55
59
|
@zoom-reset="model.resetWaveformViewport"
|
|
56
60
|
/>
|
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
import { InputNumber, Select, Switch } from 'ant-design-vue'
|
|
3
3
|
import { ColorPicker } from 'vue3-colorpicker'
|
|
4
4
|
|
|
5
|
+
import DemoYAxisPaddingControls from './DemoYAxisPaddingControls.vue'
|
|
5
6
|
import type { DemoControlPanelModel } from './types'
|
|
6
7
|
|
|
7
8
|
const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
8
9
|
</script>
|
|
9
10
|
|
|
10
11
|
<template>
|
|
12
|
+
<DemoYAxisPaddingControls v-model:model="model" />
|
|
11
13
|
<section class="control-section">
|
|
12
14
|
<h2>绘图区边距</h2>
|
|
13
15
|
<div class="plot-margin-controls">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { InputNumber, Radio, Select, Switch } from 'ant-design-vue'
|
|
3
3
|
|
|
4
4
|
import type { DemoControlPanelModel } from './types'
|
|
5
5
|
|
|
@@ -36,7 +36,10 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
|
36
36
|
</section>
|
|
37
37
|
<section class="control-section">
|
|
38
38
|
<h2>视图</h2>
|
|
39
|
-
<
|
|
39
|
+
<label class="frame-style-control frame-style-control--switch">
|
|
40
|
+
<span>内置工具栏</span>
|
|
41
|
+
<Switch v-model:checked="model.toolbarVisible" size="small" aria-label="显示内置工具栏" />
|
|
42
|
+
</label>
|
|
40
43
|
<div class="auxiliary-style-controls" style="margin-top: 10px">
|
|
41
44
|
<label class="frame-style-control frame-style-control--switch">
|
|
42
45
|
<span>整数缩放</span>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { InputNumber, Switch } from 'ant-design-vue'
|
|
3
|
+
import type { DemoControlPanelModel } from './types'
|
|
4
|
+
const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
5
|
+
</script>
|
|
6
|
+
|
|
7
|
+
<template>
|
|
8
|
+
<section class="control-section">
|
|
9
|
+
<h2>Y 轴留白</h2>
|
|
10
|
+
<div v-for="side in ['upper', 'lower'] as const" :key="side" class="y-axis-padding-control">
|
|
11
|
+
<span>{{ side === 'upper' ? '上方留白' : '下方留白' }}</span>
|
|
12
|
+
<Switch
|
|
13
|
+
v-model:checked="model[`${side}PaddingEnabled`]"
|
|
14
|
+
size="small"
|
|
15
|
+
:aria-label="side === 'upper' ? '上方留白' : '下方留白'"
|
|
16
|
+
/>
|
|
17
|
+
<InputNumber
|
|
18
|
+
v-model:value="model[`${side}PaddingPercent`]"
|
|
19
|
+
:min="0"
|
|
20
|
+
:step="1"
|
|
21
|
+
:disabled="!model[`${side}PaddingEnabled`]"
|
|
22
|
+
addon-after="%"
|
|
23
|
+
size="small"
|
|
24
|
+
:aria-label="side === 'upper' ? '上方留白比例' : '下方留白比例'"
|
|
25
|
+
/>
|
|
26
|
+
</div>
|
|
27
|
+
<small>仅作用于自动 Y 范围;刻度取整可能增加实际留白。</small>
|
|
28
|
+
</section>
|
|
29
|
+
</template>
|
|
30
|
+
|
|
31
|
+
<style scoped>
|
|
32
|
+
.y-axis-padding-control {
|
|
33
|
+
display: grid;
|
|
34
|
+
grid-template-columns: 60px 28px minmax(0, 1fr);
|
|
35
|
+
align-items: center;
|
|
36
|
+
gap: 8px;
|
|
37
|
+
margin-bottom: 10px;
|
|
38
|
+
font-size: 12px;
|
|
39
|
+
}
|
|
40
|
+
.y-axis-padding-control :deep(.ant-switch) {
|
|
41
|
+
justify-self: start;
|
|
42
|
+
}
|
|
43
|
+
.y-axis-padding-control :deep(.ant-input-number) {
|
|
44
|
+
width: 100%;
|
|
45
|
+
}
|
|
46
|
+
</style>
|