waveform-analysis 0.1.51 → 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 +13 -7
- package/dist/assets/{workerRuntime-Dt94jbqq.js → workerRuntime-BcYng9TR.js} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +557 -547
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +1 -0
- package/dist/types/components/interaction/zoomConstraints.d.ts +1 -0
- 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/useWaveformChartLifecycle.ts +1 -0
- package/src/components/core/useWaveformRenderSampling.ts +8 -7
- package/src/components/core/waveformChartTypes.ts +1 -0
- package/src/components/interaction/useWaveformZoom.ts +1 -1
- package/src/components/interaction/zoomConstraints.ts +9 -4
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +63 -3
- 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 +17 -7
|
@@ -334,6 +334,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
|
|
|
334
334
|
pannable: boolean;
|
|
335
335
|
minZoomSpan?: number | undefined;
|
|
336
336
|
minVisiblePoints: number;
|
|
337
|
+
maxZoomScale?: number | null | undefined;
|
|
337
338
|
initialXDomains?: Record<string, [number, number]> | undefined;
|
|
338
339
|
xDomainStrategy: import("../..").WaveformXDomainStrategy;
|
|
339
340
|
yDomain?: [number, number] | undefined;
|
|
@@ -14,6 +14,7 @@ export interface WaveformChartProps {
|
|
|
14
14
|
pannable?: boolean;
|
|
15
15
|
minZoomSpan?: number;
|
|
16
16
|
minVisiblePoints?: number;
|
|
17
|
+
maxZoomScale?: number | null;
|
|
17
18
|
initialXDomain?: [number, number];
|
|
18
19
|
initialXDomains?: Record<string, [number, number]>;
|
|
19
20
|
xDomainStrategy?: WaveformXDomainStrategy;
|
|
@@ -9,6 +9,7 @@ export type ZoomSeriesGroup = readonly PointSeriesSource[];
|
|
|
9
9
|
interface ZoomConstraintOptions {
|
|
10
10
|
minZoomSpan?: number;
|
|
11
11
|
minVisiblePoints?: number;
|
|
12
|
+
maxZoomScale?: number | null;
|
|
12
13
|
}
|
|
13
14
|
export declare function resolveMinimumZoomSpan(boundary: [number, number], groups: readonly ZoomSeriesGroup[], options: ZoomConstraintOptions): number;
|
|
14
15
|
export declare function constrainZoomDomain(domain: [number, number], boundary: [number, number], groups: readonly ZoomSeriesGroup[], options: ZoomConstraintOptions): [number, number];
|
|
@@ -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
|
@@ -213,6 +213,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
213
213
|
isPresentationMode,
|
|
214
214
|
() => props.minZoomSpan,
|
|
215
215
|
() => props.minVisiblePoints,
|
|
216
|
+
() => props.maxZoomScale,
|
|
216
217
|
() => props.initialXDomain,
|
|
217
218
|
() => props.initialXDomains,
|
|
218
219
|
() => props.xDomainStrategy,
|
|
@@ -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,
|
|
@@ -35,6 +35,7 @@ export interface WaveformChartProps {
|
|
|
35
35
|
pannable?: boolean
|
|
36
36
|
minZoomSpan?: number
|
|
37
37
|
minVisiblePoints?: number
|
|
38
|
+
maxZoomScale?: number | null
|
|
38
39
|
initialXDomain?: [number, number]
|
|
39
40
|
initialXDomains?: Record<string, [number, number]>
|
|
40
41
|
xDomainStrategy?: WaveformXDomainStrategy
|
|
@@ -231,7 +231,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
231
231
|
const minimumSpan = resolveMinimumZoomSpan(domain, groups, props)
|
|
232
232
|
return Math.max(
|
|
233
233
|
ZOOM_CONSTRAINTS.MIN_SCALE,
|
|
234
|
-
minimumSpan > 0 ? domainSpan / minimumSpan :
|
|
234
|
+
minimumSpan > 0 ? domainSpan / minimumSpan : Number.POSITIVE_INFINITY,
|
|
235
235
|
)
|
|
236
236
|
}
|
|
237
237
|
const constrainTransform = (
|
|
@@ -14,6 +14,7 @@ export type ZoomSeriesGroup = readonly PointSeriesSource[]
|
|
|
14
14
|
interface ZoomConstraintOptions {
|
|
15
15
|
minZoomSpan?: number
|
|
16
16
|
minVisiblePoints?: number
|
|
17
|
+
maxZoomScale?: number | null
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
const xValueCache = new WeakMap<object, Map<string, number[]>>()
|
|
@@ -80,10 +81,14 @@ export function resolveMinimumZoomSpan(
|
|
|
80
81
|
: 0
|
|
81
82
|
const required = resolveRequiredPointCount(options.minVisiblePoints)
|
|
82
83
|
const pointSpan = minimumPointSpan(groups, normalized, required)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
const configuredScale = Number(options.maxZoomScale)
|
|
85
|
+
const scaleSpan =
|
|
86
|
+
Number.isFinite(configuredScale) && configuredScale >= ZOOM_CONSTRAINTS.MIN_SCALE
|
|
87
|
+
? boundarySpan / configuredScale
|
|
88
|
+
: options.maxZoomScale !== null && configuredSpan === 0 && required === 0
|
|
89
|
+
? boundarySpan / ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE
|
|
90
|
+
: 0
|
|
91
|
+
return Math.max(configuredSpan, pointSpan, scaleSpan)
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
function expandToMinimumPoints(
|
|
@@ -65,10 +65,10 @@ function dispatchBox(
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
describe('WaveformChart point-aware zoom constraints', () => {
|
|
68
|
-
it('
|
|
68
|
+
it('zooms to two distinct X values when the scale limit is disabled', async () => {
|
|
69
69
|
const wrapper = await mountSizedChart(
|
|
70
70
|
{ kind: 'points', points: regularPoints },
|
|
71
|
-
{ minVisiblePoints: 2 },
|
|
71
|
+
{ minVisiblePoints: 2, maxZoomScale: null },
|
|
72
72
|
)
|
|
73
73
|
const { overlay, width, height } = prepareOverlay(
|
|
74
74
|
wrapper,
|
|
@@ -151,7 +151,7 @@ describe('WaveformChart point-aware zoom constraints', () => {
|
|
|
151
151
|
expect(visibleXCount(constrainedDomain)).toBe(2)
|
|
152
152
|
})
|
|
153
153
|
|
|
154
|
-
it('retains the default
|
|
154
|
+
it('retains the backwards-compatible default maximum zoom scale', async () => {
|
|
155
155
|
const wrapper = await mountSizedChart({ kind: 'points', points: regularPoints })
|
|
156
156
|
const { overlay, width, height } = prepareOverlay(
|
|
157
157
|
wrapper,
|
|
@@ -163,6 +163,66 @@ describe('WaveformChart point-aware zoom constraints', () => {
|
|
|
163
163
|
expect(domain[1] - domain[0]).toBeCloseTo(1_199 / 40)
|
|
164
164
|
})
|
|
165
165
|
|
|
166
|
+
it('honors an explicit maximum zoom scale', async () => {
|
|
167
|
+
const wrapper = await mountSizedChart(
|
|
168
|
+
{ kind: 'points', points: regularPoints },
|
|
169
|
+
{ maxZoomScale: 20 },
|
|
170
|
+
)
|
|
171
|
+
const { overlay, width, height } = prepareOverlay(
|
|
172
|
+
wrapper,
|
|
173
|
+
'.waveform-chart__overlay--independent',
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
await dispatchWheel(overlay, width, height, -4_000)
|
|
177
|
+
const domain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
178
|
+
expect(domain[1] - domain[0]).toBeCloseTo(1_199 / 20)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('disables the compatibility scale limit when configured with null', async () => {
|
|
182
|
+
const wrapper = await mountSizedChart(
|
|
183
|
+
{ kind: 'points', points: regularPoints },
|
|
184
|
+
{ maxZoomScale: null },
|
|
185
|
+
)
|
|
186
|
+
const { overlay, width, height } = prepareOverlay(
|
|
187
|
+
wrapper,
|
|
188
|
+
'.waveform-chart__overlay--independent',
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
await dispatchWheel(overlay, width, height, -4_000)
|
|
192
|
+
const domain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
193
|
+
expect(domain[1] - domain[0]).toBeLessThan(1_199 / 40)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('falls back to the compatibility limit for an invalid maximum scale', async () => {
|
|
197
|
+
const wrapper = await mountSizedChart(
|
|
198
|
+
{ kind: 'points', points: regularPoints },
|
|
199
|
+
{ maxZoomScale: 0 },
|
|
200
|
+
)
|
|
201
|
+
const { overlay, width, height } = prepareOverlay(
|
|
202
|
+
wrapper,
|
|
203
|
+
'.waveform-chart__overlay--independent',
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
await dispatchWheel(overlay, width, height, -4_000)
|
|
207
|
+
const domain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
208
|
+
expect(domain[1] - domain[0]).toBeCloseTo(1_199 / 40)
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('uses the strictest configured zoom constraint', async () => {
|
|
212
|
+
const wrapper = await mountSizedChart(
|
|
213
|
+
{ kind: 'points', points: regularPoints },
|
|
214
|
+
{ minZoomSpan: 50, minVisiblePoints: 2, maxZoomScale: 40 },
|
|
215
|
+
)
|
|
216
|
+
const { overlay, width, height } = prepareOverlay(
|
|
217
|
+
wrapper,
|
|
218
|
+
'.waveform-chart__overlay--independent',
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
await dispatchWheel(overlay, width, height, -4_000)
|
|
222
|
+
const domain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
|
223
|
+
expect(domain[1] - domain[0]).toBeCloseTo(50)
|
|
224
|
+
})
|
|
225
|
+
|
|
166
226
|
it('retains two endpoint samples while replacing an independent data window', async () => {
|
|
167
227
|
const sourcePoints = Array.from({ length: 1_000 }, (_, index) => ({
|
|
168
228
|
x: -5 + (index * 10) / 999,
|
|
@@ -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,7 +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)
|
|
72
|
+
const minVisiblePoints = ref(2)
|
|
73
|
+
const annotations = ref<WaveformAnnotation[]>([])
|
|
71
74
|
const diagnostics = reactive<Record<string, WaveformSamplingDiagnostics | undefined>>({})
|
|
72
75
|
|
|
73
76
|
const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
@@ -75,7 +78,7 @@ const rendering = computed<WaveformRenderingOptions>(() => ({
|
|
|
75
78
|
mode: mode.value,
|
|
76
79
|
strategy: strategy.value,
|
|
77
80
|
autoThreshold: autoThreshold.value,
|
|
78
|
-
|
|
81
|
+
maxPointCount: maxPointCount.value,
|
|
79
82
|
rawPointLimit: 100_000,
|
|
80
83
|
wasmFailureFallback: 'javascript',
|
|
81
84
|
},
|
|
@@ -126,24 +129,31 @@ function pointCount(value: number | undefined) {
|
|
|
126
129
|
/>
|
|
127
130
|
</label>
|
|
128
131
|
<label>
|
|
129
|
-
<span
|
|
132
|
+
<span>最多渲染点</span>
|
|
130
133
|
<InputNumber
|
|
131
|
-
v-model:value="
|
|
132
|
-
:min="
|
|
133
|
-
:max="
|
|
134
|
-
:step="
|
|
134
|
+
v-model:value="maxPointCount"
|
|
135
|
+
:min="1"
|
|
136
|
+
:max="100000"
|
|
137
|
+
:step="100"
|
|
135
138
|
size="small"
|
|
136
139
|
/>
|
|
137
140
|
</label>
|
|
141
|
+
<label>
|
|
142
|
+
<span>最少可见点</span>
|
|
143
|
+
<InputNumber v-model:value="minVisiblePoints" :min="2" :max="1000" :step="1" size="small" />
|
|
144
|
+
</label>
|
|
138
145
|
</section>
|
|
139
146
|
|
|
140
147
|
<section class="sampling-demo__workspace">
|
|
141
148
|
<div class="sampling-demo__chart">
|
|
142
149
|
<WaveformChart
|
|
143
150
|
:data="data"
|
|
151
|
+
v-model:annotations="annotations"
|
|
144
152
|
:rendering="rendering"
|
|
145
153
|
:grid="{ rowCount: 1, columnCount: 1, showPagination: false }"
|
|
146
154
|
:legend="{ position: 'top-right', orientation: 'vertical' }"
|
|
155
|
+
:max-zoom-scale="null"
|
|
156
|
+
:min-visible-points="minVisiblePoints"
|
|
147
157
|
:title="{ visible: true, text: '多系列采样与缓存诊断', align: 'left' }"
|
|
148
158
|
x-label="时间(s)"
|
|
149
159
|
time-unit="s"
|