waveform-analysis 0.1.52 → 0.1.53
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 +8 -5
- package/dist/assets/{workerRuntime-Dt94jbqq.js → workerRuntime-BcYng9TR.js} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +553 -545
- package/dist/types/core/renderingOptions.d.ts +1 -0
- package/dist/types/core/workerSampling/protocol.d.ts +1 -0
- package/dist/types/types/chart.d.ts +2 -0
- package/package.json +1 -1
- package/src/components/core/useWaveformRenderSampling.ts +8 -7
- package/src/core/rendering.test.ts +3 -0
- package/src/core/rendering.ts +3 -1
- package/src/core/renderingOptions.ts +9 -0
- package/src/core/renderingStrategies.ts +6 -2
- package/src/core/workerSampling/protocol.ts +1 -0
- package/src/core/workerSampling/repositorySampling.test.ts +52 -0
- package/src/core/workerSampling/repositorySupport.ts +4 -0
- package/src/types/chart.ts +2 -0
- package/src/views/WasmSamplingDemo.vue +10 -7
|
@@ -70,6 +70,7 @@ export interface WorkerSamplingSeriesRequest {
|
|
|
70
70
|
previousSelectedMode?: 'raw' | 'sampled';
|
|
71
71
|
strategy: WaveformSamplingStrategy;
|
|
72
72
|
maxPointsPerPixel?: number;
|
|
73
|
+
maxPointCount?: number;
|
|
73
74
|
rawPointLimit?: number;
|
|
74
75
|
wasmFailureFallback?: 'error' | 'javascript';
|
|
75
76
|
/** Rendering inputs that affect the cache key, even when only line geometry is overridden. */
|
|
@@ -84,6 +84,8 @@ export interface WaveformSamplingOptions {
|
|
|
84
84
|
strategy?: WaveformSamplingStrategy;
|
|
85
85
|
/** Upper bound for rendered points per horizontal CSS pixel. */
|
|
86
86
|
maxPointsPerPixel?: number;
|
|
87
|
+
/** Fixed per-series target point count. When set, this takes precedence over pixel density. */
|
|
88
|
+
maxPointCount?: number;
|
|
87
89
|
/** Visible-point count at which raw rendering should report a diagnostic. */
|
|
88
90
|
rawPointLimit?: number;
|
|
89
91
|
/** Controls whether a forced WASM failure may temporarily show JavaScript results. */
|
package/package.json
CHANGED
|
@@ -75,11 +75,14 @@ function rawDiagnostics(target: SamplingTarget, requestId: number): WorkerSampli
|
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function fallbackPoints(target: SamplingTarget
|
|
78
|
+
function fallbackPoints(target: SamplingTarget) {
|
|
79
79
|
const { source } = target
|
|
80
80
|
const { start, end } = target.visibleRange
|
|
81
81
|
const count = end - start
|
|
82
|
-
const targetCount =
|
|
82
|
+
const targetCount =
|
|
83
|
+
Number.isFinite(target.request.maxPointCount) && (target.request.maxPointCount ?? 0) >= 1
|
|
84
|
+
? Math.floor(target.request.maxPointCount as number)
|
|
85
|
+
: Math.max(1, Math.floor(target.request.plotWidth * target.request.maxPointsPerPixel!))
|
|
83
86
|
if (count <= targetCount) return source.pointsInRange(start, end)
|
|
84
87
|
if (targetCount === 1) return source.pointAt(start) ? [source.pointAt(start)!] : []
|
|
85
88
|
const points: WaveformPoint[] = []
|
|
@@ -141,6 +144,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
141
144
|
autoHysteresis: sampling.autoHysteresis,
|
|
142
145
|
strategy: sampling.strategy,
|
|
143
146
|
maxPointsPerPixel: sampling.maxPointsPerPixel,
|
|
147
|
+
maxPointCount: sampling.maxPointCount,
|
|
144
148
|
rawPointLimit: sampling.rawPointLimit,
|
|
145
149
|
wasmFailureFallback: sampling.wasmFailureFallback,
|
|
146
150
|
lineType: series.lineType,
|
|
@@ -159,7 +163,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
159
163
|
`${dataEpoch.value}:${targets.value
|
|
160
164
|
.map(
|
|
161
165
|
(target) =>
|
|
162
|
-
`${target.request.datasetId}:${target.request.xDomain.join(',')}:${target.request.plotWidth}:${target.visibleRange.start}:${target.visibleRange.end}:${target.request.mode}:${target.request.autoThreshold}:${target.request.autoHysteresis}:${target.request.strategy}:${target.request.maxPointsPerPixel}:${target.request.rawPointLimit}:${target.request.wasmFailureFallback}:${target.request.lineType}:${target.request.pointType}:${target.request.errorBarVisible}:${target.request.pointMinSpacing}:${target.request.errorBarMinSpacing}`,
|
|
166
|
+
`${target.request.datasetId}:${target.request.xDomain.join(',')}:${target.request.plotWidth}:${target.visibleRange.start}:${target.visibleRange.end}:${target.request.mode}:${target.request.autoThreshold}:${target.request.autoHysteresis}:${target.request.strategy}:${target.request.maxPointsPerPixel}:${target.request.maxPointCount}:${target.request.rawPointLimit}:${target.request.wasmFailureFallback}:${target.request.lineType}:${target.request.pointType}:${target.request.errorBarVisible}:${target.request.pointMinSpacing}:${target.request.errorBarMinSpacing}`,
|
|
163
167
|
)
|
|
164
168
|
.join('|')}`,
|
|
165
169
|
)
|
|
@@ -253,10 +257,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
|
|
|
253
257
|
diagnostics.push(rawDiagnostics(target, token))
|
|
254
258
|
} else {
|
|
255
259
|
if (!nextOverrides[target.series.id]?.length) {
|
|
256
|
-
nextOverrides[target.series.id] = fallbackPoints(
|
|
257
|
-
target,
|
|
258
|
-
context.renderingOptions.value.sampling.maxPointsPerPixel,
|
|
259
|
-
)
|
|
260
|
+
nextOverrides[target.series.id] = fallbackPoints(target)
|
|
260
261
|
}
|
|
261
262
|
sampledTargets.push({
|
|
262
263
|
...target,
|
|
@@ -69,6 +69,7 @@ describe('waveform rendering selection', () => {
|
|
|
69
69
|
autoHysteresis: 20.8,
|
|
70
70
|
strategy: 'minmax',
|
|
71
71
|
maxPointsPerPixel: 2.5,
|
|
72
|
+
maxPointCount: 1_000.9,
|
|
72
73
|
rawPointLimit: 50_000.2,
|
|
73
74
|
wasmFailureFallback: 'javascript',
|
|
74
75
|
},
|
|
@@ -83,6 +84,7 @@ describe('waveform rendering selection', () => {
|
|
|
83
84
|
autoHysteresis: 20,
|
|
84
85
|
strategy: 'minmax',
|
|
85
86
|
maxPointsPerPixel: 2.5,
|
|
87
|
+
maxPointCount: 1_000,
|
|
86
88
|
rawPointLimit: 50_000,
|
|
87
89
|
wasmFailureFallback: 'javascript',
|
|
88
90
|
},
|
|
@@ -96,6 +98,7 @@ describe('waveform rendering selection', () => {
|
|
|
96
98
|
autoHysteresis: -1,
|
|
97
99
|
strategy: 'invalid',
|
|
98
100
|
maxPointsPerPixel: Number.POSITIVE_INFINITY,
|
|
101
|
+
maxPointCount: 0,
|
|
99
102
|
rawPointLimit: 0,
|
|
100
103
|
wasmFailureFallback: 'fallback',
|
|
101
104
|
},
|
package/src/core/rendering.ts
CHANGED
|
@@ -106,7 +106,9 @@ function selectSamplingPlaceholderPoints(
|
|
|
106
106
|
const count = end - start
|
|
107
107
|
if (count <= 0) return []
|
|
108
108
|
if (options.sampling.strategy === 'none') return points.slice(start, end)
|
|
109
|
-
const target =
|
|
109
|
+
const target =
|
|
110
|
+
options.sampling.maxPointCount ??
|
|
111
|
+
Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
|
|
110
112
|
if (count <= target) return points.slice(start, end)
|
|
111
113
|
if (target === 1) return [points[start]!]
|
|
112
114
|
return Array.from({ length: target }, (_, index) => {
|
|
@@ -11,6 +11,7 @@ export interface ResolvedWaveformSamplingOptions {
|
|
|
11
11
|
autoHysteresis: number
|
|
12
12
|
strategy: WaveformSamplingStrategy
|
|
13
13
|
maxPointsPerPixel: number
|
|
14
|
+
maxPointCount: number | undefined
|
|
14
15
|
rawPointLimit: number
|
|
15
16
|
wasmFailureFallback: 'error' | 'javascript'
|
|
16
17
|
}
|
|
@@ -36,6 +37,7 @@ export const DEFAULT_WAVEFORM_RENDERING_OPTIONS: ResolvedWaveformRenderingOption
|
|
|
36
37
|
autoHysteresis: 0,
|
|
37
38
|
strategy: 'peak',
|
|
38
39
|
maxPointsPerPixel: 4,
|
|
40
|
+
maxPointCount: undefined,
|
|
39
41
|
rawPointLimit: 100_000,
|
|
40
42
|
wasmFailureFallback: 'error',
|
|
41
43
|
},
|
|
@@ -72,6 +74,12 @@ function resolvePositiveNumber(value: unknown, fallback: number): number {
|
|
|
72
74
|
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
function resolveOptionalFiniteInteger(value: unknown, minimum: number): number | undefined {
|
|
78
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= minimum
|
|
79
|
+
? Math.floor(value)
|
|
80
|
+
: undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
75
83
|
function resolveNonNegativeNumber(value: number, fallback: number): number {
|
|
76
84
|
return Number.isFinite(value) && value >= 0 ? value : fallback
|
|
77
85
|
}
|
|
@@ -106,6 +114,7 @@ function resolveSamplingOptions(
|
|
|
106
114
|
? options.strategy
|
|
107
115
|
: DEFAULT_WAVEFORM_RENDERING_OPTIONS.sampling.strategy,
|
|
108
116
|
maxPointsPerPixel,
|
|
117
|
+
maxPointCount: resolveOptionalFiniteInteger(options?.maxPointCount, 1),
|
|
109
118
|
rawPointLimit: resolveFiniteInteger(
|
|
110
119
|
options?.rawPointLimit,
|
|
111
120
|
1,
|
|
@@ -42,7 +42,10 @@ export const peakPreservingPointSelectionStrategy: RenderablePointSelectionStrat
|
|
|
42
42
|
|
|
43
43
|
const domainStart = Math.min(domain[0], domain[1])
|
|
44
44
|
const domainEnd = Math.max(domain[0], domain[1])
|
|
45
|
-
const maximumPointCount = Math.max(
|
|
45
|
+
const maximumPointCount = Math.max(
|
|
46
|
+
4,
|
|
47
|
+
options.sampling.maxPointCount ?? Math.floor(width * options.maxPointsPerPixel),
|
|
48
|
+
)
|
|
46
49
|
const bucketCount = Math.max(1, Math.floor(maximumPointCount / 4))
|
|
47
50
|
const result: WaveformPoint[] = []
|
|
48
51
|
const span = domainEnd - domainStart || 1
|
|
@@ -119,7 +122,8 @@ export function resolveRenderablePointSelectionStrategy(
|
|
|
119
122
|
): RenderablePointSelectionStrategy {
|
|
120
123
|
const maximumPointCount = Math.max(
|
|
121
124
|
4,
|
|
122
|
-
|
|
125
|
+
request.options.sampling.maxPointCount ??
|
|
126
|
+
Math.floor(request.width * request.options.maxPointsPerPixel),
|
|
123
127
|
)
|
|
124
128
|
const shouldUseCompletePoints =
|
|
125
129
|
!request.options.downsample ||
|
|
@@ -90,6 +90,7 @@ export interface WorkerSamplingSeriesRequest {
|
|
|
90
90
|
previousSelectedMode?: 'raw' | 'sampled'
|
|
91
91
|
strategy: WaveformSamplingStrategy
|
|
92
92
|
maxPointsPerPixel?: number
|
|
93
|
+
maxPointCount?: number
|
|
93
94
|
rawPointLimit?: number
|
|
94
95
|
wasmFailureFallback?: 'error' | 'javascript'
|
|
95
96
|
/** Rendering inputs that affect the cache key, even when only line geometry is overridden. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import type { WaveformPoint } from '@/types'
|
|
4
|
+
import type { WorkerSamplingSeriesRequest } from './protocol'
|
|
5
|
+
import { WorkerSamplingRepository } from './repository'
|
|
6
|
+
|
|
7
|
+
const points: WaveformPoint[] = Array.from({ length: 12 }, (_, index) => ({
|
|
8
|
+
x: index,
|
|
9
|
+
y: index === 5 ? 100 : index,
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
function register(repository: WorkerSamplingRepository, datasetId: string) {
|
|
13
|
+
return repository.handle({
|
|
14
|
+
type: 'register-dataset',
|
|
15
|
+
requestId: 1,
|
|
16
|
+
datasetId,
|
|
17
|
+
revision: 0,
|
|
18
|
+
points,
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function sampleSeries(datasetId: string): WorkerSamplingSeriesRequest {
|
|
23
|
+
return {
|
|
24
|
+
seriesId: `${datasetId}-series`,
|
|
25
|
+
datasetId,
|
|
26
|
+
revision: 1,
|
|
27
|
+
xDomain: [0, 11],
|
|
28
|
+
plotWidth: 1_000,
|
|
29
|
+
mode: 'auto',
|
|
30
|
+
autoThreshold: 2,
|
|
31
|
+
strategy: 'peak',
|
|
32
|
+
maxPointsPerPixel: 4,
|
|
33
|
+
maxPointCount: 4,
|
|
34
|
+
rawPointLimit: 5,
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('WorkerSamplingRepository sampling targets', () => {
|
|
39
|
+
it('uses a fixed maximum point count instead of pixel density when configured', () => {
|
|
40
|
+
const repository = new WorkerSamplingRepository()
|
|
41
|
+
register(repository, 'fixed-target')
|
|
42
|
+
const response = repository.handle({
|
|
43
|
+
type: 'sample-viewport',
|
|
44
|
+
requestId: 51,
|
|
45
|
+
series: [sampleSeries('fixed-target')],
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
expect(response.type).toBe('sample-viewport-response')
|
|
49
|
+
if (response.type !== 'sample-viewport-response') return
|
|
50
|
+
expect(response.results[0]?.diagnostics.renderedPointCount).toBeLessThanOrEqual(4)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
@@ -92,6 +92,9 @@ export function selectedStrategy(strategy: WorkerSamplingSeriesRequest['strategy
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
export function targetPointCount(request: WorkerSamplingSeriesRequest) {
|
|
95
|
+
if (Number.isFinite(request.maxPointCount) && (request.maxPointCount ?? 0) >= 1) {
|
|
96
|
+
return Math.floor(request.maxPointCount as number)
|
|
97
|
+
}
|
|
95
98
|
const width = Number.isFinite(request.plotWidth) ? Math.max(0, request.plotWidth) : 0
|
|
96
99
|
const density = Number.isFinite(request.maxPointsPerPixel)
|
|
97
100
|
? Math.max(0, request.maxPointsPerPixel ?? 0)
|
|
@@ -179,6 +182,7 @@ export function cacheKey(
|
|
|
179
182
|
request.plotWidth,
|
|
180
183
|
target,
|
|
181
184
|
request.maxPointsPerPixel,
|
|
185
|
+
request.maxPointCount,
|
|
182
186
|
request.mode,
|
|
183
187
|
backend,
|
|
184
188
|
strategy,
|
package/src/types/chart.ts
CHANGED
|
@@ -96,6 +96,8 @@ export interface WaveformSamplingOptions {
|
|
|
96
96
|
strategy?: WaveformSamplingStrategy
|
|
97
97
|
/** Upper bound for rendered points per horizontal CSS pixel. */
|
|
98
98
|
maxPointsPerPixel?: number
|
|
99
|
+
/** Fixed per-series target point count. When set, this takes precedence over pixel density. */
|
|
100
|
+
maxPointCount?: number
|
|
99
101
|
/** Visible-point count at which raw rendering should report a diagnostic. */
|
|
100
102
|
rawPointLimit?: number
|
|
101
103
|
/** Controls whether a forced WASM failure may temporarily show JavaScript results. */
|
|
@@ -5,6 +5,7 @@ import { InputNumber, RadioButton, RadioGroup, Select } from 'ant-design-vue'
|
|
|
5
5
|
import { WaveformChart } from '../components'
|
|
6
6
|
import type {
|
|
7
7
|
WaveformData,
|
|
8
|
+
WaveformAnnotation,
|
|
8
9
|
WaveformRenderingOptions,
|
|
9
10
|
WaveformSamplingDiagnostics,
|
|
10
11
|
WaveformSamplingMode,
|
|
@@ -67,8 +68,9 @@ const data = createDenseData()
|
|
|
67
68
|
const mode = ref<WaveformSamplingMode>('auto')
|
|
68
69
|
const strategy = ref<WaveformSamplingStrategy>('peak')
|
|
69
70
|
const autoThreshold = ref(1_000)
|
|
70
|
-
const
|
|
71
|
+
const maxPointCount = ref(1_000)
|
|
71
72
|
const minVisiblePoints = ref(2)
|
|
73
|
+
const annotations = ref<WaveformAnnotation[]>([])
|
|
72
74
|
const diagnostics = reactive<Record<string, WaveformSamplingDiagnostics | undefined>>({})
|
|
73
75
|
|
|
74
76
|
const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
@@ -76,7 +78,7 @@ const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
|
76
78
|
mode: mode.value,
|
|
77
79
|
strategy: strategy.value,
|
|
78
80
|
autoThreshold: autoThreshold.value,
|
|
79
|
-
|
|
81
|
+
maxPointCount: maxPointCount.value,
|
|
80
82
|
rawPointLimit: 100_000,
|
|
81
83
|
wasmFailureFallback: 'javascript',
|
|
82
84
|
},
|
|
@@ -127,12 +129,12 @@ function pointCount(value: number | undefined) {
|
|
|
127
129
|
/>
|
|
128
130
|
</label>
|
|
129
131
|
<label>
|
|
130
|
-
<span
|
|
132
|
+
<span>最多渲染点</span>
|
|
131
133
|
<InputNumber
|
|
132
|
-
v-model:value="
|
|
133
|
-
:min="
|
|
134
|
-
:max="
|
|
135
|
-
:step="
|
|
134
|
+
v-model:value="maxPointCount"
|
|
135
|
+
:min="1"
|
|
136
|
+
:max="100000"
|
|
137
|
+
:step="100"
|
|
136
138
|
size="small"
|
|
137
139
|
/>
|
|
138
140
|
</label>
|
|
@@ -146,6 +148,7 @@ function pointCount(value: number | undefined) {
|
|
|
146
148
|
<div class="sampling-demo__chart">
|
|
147
149
|
<WaveformChart
|
|
148
150
|
:data="data"
|
|
151
|
+
v-model:annotations="annotations"
|
|
149
152
|
:rendering="rendering"
|
|
150
153
|
:grid="{ rowCount: 1, columnCount: 1, showPagination: false }"
|
|
151
154
|
:legend="{ position: 'top-right', orientation: 'vertical' }"
|