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.
@@ -5,6 +5,7 @@ export interface ResolvedWaveformSamplingOptions {
5
5
  autoHysteresis: number;
6
6
  strategy: WaveformSamplingStrategy;
7
7
  maxPointsPerPixel: number;
8
+ maxPointCount: number | undefined;
8
9
  rawPointLimit: number;
9
10
  wasmFailureFallback: 'error' | 'javascript';
10
11
  }
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.52",
3
+ "version": "0.1.53",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -75,11 +75,14 @@ function rawDiagnostics(target: SamplingTarget, requestId: number): WorkerSampli
75
75
  }
76
76
  }
77
77
 
78
- function fallbackPoints(target: SamplingTarget, maxPointsPerPixel: number) {
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 = Math.max(1, Math.floor(target.request.plotWidth * maxPointsPerPixel))
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
  },
@@ -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 = Math.max(1, Math.floor(width * options.sampling.maxPointsPerPixel))
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(4, Math.floor(width * options.maxPointsPerPixel))
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
- Math.floor(request.width * request.options.maxPointsPerPixel),
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,
@@ -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 maxPointsPerPixel = ref(3)
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
- maxPointsPerPixel: maxPointsPerPixel.value,
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>每像素点数</span>
132
+ <span>最多渲染点</span>
131
133
  <InputNumber
132
- v-model:value="maxPointsPerPixel"
133
- :min="0.25"
134
- :max="16"
135
- :step="0.25"
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' }"