waveform-analysis 0.1.52 → 0.1.54
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 +9 -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/dist/types/views/wasmSamplingDemoSampling.d.ts +6 -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 +106 -0
- package/src/core/workerSampling/repositorySupport.ts +4 -0
- package/src/types/chart.ts +2 -0
- package/src/views/WasmSamplingDemo.vue +25 -5
- package/src/views/wasmSamplingDemoSampling.test.ts +27 -0
- package/src/views/wasmSamplingDemoSampling.ts +23 -0
|
@@ -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. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { WaveformSamplingOptions, WaveformSamplingStrategy } from '../types';
|
|
2
|
+
type SamplingSizeOptions = Pick<WaveformSamplingOptions, 'maxPointCount' | 'maxPointsPerPixel'>;
|
|
3
|
+
export declare function usesFixedPointCount(strategy: WaveformSamplingStrategy): boolean;
|
|
4
|
+
export declare function showsSamplingSize(strategy: WaveformSamplingStrategy): strategy is "auto" | "peak" | "lttb" | "average" | "min" | "max" | "minmax" | "sum";
|
|
5
|
+
export declare function samplingSizeOptions(strategy: WaveformSamplingStrategy, maxPointsPerPixel: number, maxPointCount: number): SamplingSizeOptions;
|
|
6
|
+
export {};
|
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,106 @@
|
|
|
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
|
+
|
|
53
|
+
it.each(['lttb', 'average', 'sum'] as const)(
|
|
54
|
+
'returns exactly the fixed target for %s when enough points are visible',
|
|
55
|
+
(strategy) => {
|
|
56
|
+
const source = Array.from({ length: 100_000 }, (_, index) => ({
|
|
57
|
+
x: index,
|
|
58
|
+
y: Math.sin(index / 10),
|
|
59
|
+
}))
|
|
60
|
+
const repository = new WorkerSamplingRepository()
|
|
61
|
+
repository.handle({
|
|
62
|
+
type: 'register-dataset',
|
|
63
|
+
requestId: 1,
|
|
64
|
+
datasetId: strategy,
|
|
65
|
+
revision: 0,
|
|
66
|
+
points: source,
|
|
67
|
+
})
|
|
68
|
+
const response = repository.handle({
|
|
69
|
+
type: 'sample-viewport',
|
|
70
|
+
requestId: 2,
|
|
71
|
+
series: [
|
|
72
|
+
{
|
|
73
|
+
...sampleSeries(strategy),
|
|
74
|
+
xDomain: [0, 99_999],
|
|
75
|
+
strategy,
|
|
76
|
+
maxPointCount: 1_000,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
expect(response).toMatchObject({
|
|
82
|
+
results: [{ diagnostics: { visiblePointCount: 100_000, renderedPointCount: 1_000 } }],
|
|
83
|
+
})
|
|
84
|
+
},
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
it('does not add points when the visible source has fewer than the fixed target', () => {
|
|
88
|
+
const repository = new WorkerSamplingRepository()
|
|
89
|
+
register(repository, 'small-target')
|
|
90
|
+
const response = repository.handle({
|
|
91
|
+
type: 'sample-viewport',
|
|
92
|
+
requestId: 3,
|
|
93
|
+
series: [
|
|
94
|
+
{
|
|
95
|
+
...sampleSeries('small-target'),
|
|
96
|
+
strategy: 'lttb',
|
|
97
|
+
maxPointCount: 100,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
expect(response).toMatchObject({
|
|
103
|
+
results: [{ diagnostics: { visiblePointCount: 12, renderedPointCount: 12 } }],
|
|
104
|
+
})
|
|
105
|
+
})
|
|
106
|
+
})
|
|
@@ -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,11 +5,17 @@ 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,
|
|
11
12
|
WaveformSamplingStrategy,
|
|
12
13
|
} from '../types'
|
|
14
|
+
import {
|
|
15
|
+
samplingSizeOptions,
|
|
16
|
+
showsSamplingSize,
|
|
17
|
+
usesFixedPointCount,
|
|
18
|
+
} from './wasmSamplingDemoSampling'
|
|
13
19
|
|
|
14
20
|
const SERIES_COUNT = 10
|
|
15
21
|
const POINTS_PER_SERIES = 100_000
|
|
@@ -68,7 +74,9 @@ const mode = ref<WaveformSamplingMode>('auto')
|
|
|
68
74
|
const strategy = ref<WaveformSamplingStrategy>('peak')
|
|
69
75
|
const autoThreshold = ref(1_000)
|
|
70
76
|
const maxPointsPerPixel = ref(3)
|
|
77
|
+
const maxPointCount = ref(1_000)
|
|
71
78
|
const minVisiblePoints = ref(2)
|
|
79
|
+
const annotations = ref<WaveformAnnotation[]>([])
|
|
72
80
|
const diagnostics = reactive<Record<string, WaveformSamplingDiagnostics | undefined>>({})
|
|
73
81
|
|
|
74
82
|
const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
@@ -76,7 +84,7 @@ const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
|
76
84
|
mode: mode.value,
|
|
77
85
|
strategy: strategy.value,
|
|
78
86
|
autoThreshold: autoThreshold.value,
|
|
79
|
-
|
|
87
|
+
...samplingSizeOptions(strategy.value, maxPointsPerPixel.value, maxPointCount.value),
|
|
80
88
|
rawPointLimit: 100_000,
|
|
81
89
|
wasmFailureFallback: 'javascript',
|
|
82
90
|
},
|
|
@@ -126,16 +134,27 @@ function pointCount(value: number | undefined) {
|
|
|
126
134
|
size="small"
|
|
127
135
|
/>
|
|
128
136
|
</label>
|
|
129
|
-
<label>
|
|
137
|
+
<label v-if="usesFixedPointCount(strategy)">
|
|
138
|
+
<span>目标渲染点数</span>
|
|
139
|
+
<InputNumber
|
|
140
|
+
v-model:value="maxPointCount"
|
|
141
|
+
:min="1"
|
|
142
|
+
:max="100000"
|
|
143
|
+
:step="100"
|
|
144
|
+
size="small"
|
|
145
|
+
/>
|
|
146
|
+
</label>
|
|
147
|
+
<label v-else-if="showsSamplingSize(strategy)">
|
|
130
148
|
<span>每像素点数</span>
|
|
131
149
|
<InputNumber
|
|
132
150
|
v-model:value="maxPointsPerPixel"
|
|
133
|
-
:min="0.
|
|
134
|
-
:max="
|
|
135
|
-
:step="0.
|
|
151
|
+
:min="0.1"
|
|
152
|
+
:max="10"
|
|
153
|
+
:step="0.1"
|
|
136
154
|
size="small"
|
|
137
155
|
/>
|
|
138
156
|
</label>
|
|
157
|
+
<span v-else class="sampling-demo__sampling-disabled">当前策略不执行降采样</span>
|
|
139
158
|
<label>
|
|
140
159
|
<span>最少可见点</span>
|
|
141
160
|
<InputNumber v-model:value="minVisiblePoints" :min="2" :max="1000" :step="1" size="small" />
|
|
@@ -146,6 +165,7 @@ function pointCount(value: number | undefined) {
|
|
|
146
165
|
<div class="sampling-demo__chart">
|
|
147
166
|
<WaveformChart
|
|
148
167
|
:data="data"
|
|
168
|
+
v-model:annotations="annotations"
|
|
149
169
|
:rendering="rendering"
|
|
150
170
|
:grid="{ rowCount: 1, columnCount: 1, showPagination: false }"
|
|
151
171
|
:legend="{ position: 'top-right', orientation: 'vertical' }"
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
samplingSizeOptions,
|
|
5
|
+
showsSamplingSize,
|
|
6
|
+
usesFixedPointCount,
|
|
7
|
+
} from './wasmSamplingDemoSampling'
|
|
8
|
+
|
|
9
|
+
describe('WASM sampling demo sizing', () => {
|
|
10
|
+
it('uses pixel density for peak-oriented and extrema strategies', () => {
|
|
11
|
+
expect(samplingSizeOptions('auto', 3, 1_000)).toEqual({ maxPointsPerPixel: 3 })
|
|
12
|
+
expect(samplingSizeOptions('peak', 3, 1_000)).toEqual({ maxPointsPerPixel: 3 })
|
|
13
|
+
expect(samplingSizeOptions('minmax', 3, 1_000)).toEqual({ maxPointsPerPixel: 3 })
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('uses a fixed point count for LTTB and aggregate strategies', () => {
|
|
17
|
+
for (const strategy of ['lttb', 'average', 'sum'] as const) {
|
|
18
|
+
expect(usesFixedPointCount(strategy)).toBe(true)
|
|
19
|
+
expect(samplingSizeOptions(strategy, 3, 1_000)).toEqual({ maxPointCount: 1_000 })
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('does not show a sampling-size control or pass a size for none', () => {
|
|
24
|
+
expect(showsSamplingSize('none')).toBe(false)
|
|
25
|
+
expect(samplingSizeOptions('none', 3, 1_000)).toEqual({})
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WaveformSamplingOptions, WaveformSamplingStrategy } from '../types'
|
|
2
|
+
|
|
3
|
+
type SamplingSizeOptions = Pick<WaveformSamplingOptions, 'maxPointCount' | 'maxPointsPerPixel'>
|
|
4
|
+
|
|
5
|
+
const fixedPointCountStrategies: readonly WaveformSamplingStrategy[] = ['lttb', 'average', 'sum']
|
|
6
|
+
|
|
7
|
+
export function usesFixedPointCount(strategy: WaveformSamplingStrategy) {
|
|
8
|
+
return fixedPointCountStrategies.includes(strategy)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function showsSamplingSize(strategy: WaveformSamplingStrategy) {
|
|
12
|
+
return strategy !== 'none'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function samplingSizeOptions(
|
|
16
|
+
strategy: WaveformSamplingStrategy,
|
|
17
|
+
maxPointsPerPixel: number,
|
|
18
|
+
maxPointCount: number,
|
|
19
|
+
): SamplingSizeOptions {
|
|
20
|
+
if (usesFixedPointCount(strategy)) return { maxPointCount }
|
|
21
|
+
if (!showsSamplingSize(strategy)) return {}
|
|
22
|
+
return { maxPointsPerPixel }
|
|
23
|
+
}
|