waveform-analysis 0.1.75 → 0.1.77

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.
Files changed (78) hide show
  1. package/README.md +183 -42
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +1 -1
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/animation/context.d.ts +1 -0
  6. package/dist/types/components/animation/paginationPresentation.d.ts +7 -7
  7. package/dist/types/components/animation/useWaveformAnimations.d.ts +1 -18
  8. package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
  9. package/dist/types/components/core/layout.d.ts +6 -9
  10. package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
  11. package/dist/types/components/core/useWaveformChartController.d.ts +2 -19
  12. package/dist/types/components/core/useWaveformData.d.ts +2 -2
  13. package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
  14. package/dist/types/components/core/waveformChartTypes.d.ts +4 -2
  15. package/dist/types/components/core/xAxisTicks.d.ts +2 -0
  16. package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
  17. package/dist/types/components/core/yAxisTicks.d.ts +6 -0
  18. package/dist/types/components/data/types.d.ts +1 -1
  19. package/dist/types/components/interaction/integerZoom.d.ts +3 -3
  20. package/dist/types/components/interaction/stepZoom.d.ts +5 -0
  21. package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
  22. package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
  23. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +0 -1
  24. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +0 -1
  25. package/dist/types/core/data.d.ts +1 -1
  26. package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
  27. package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
  28. package/dist/types/demo/types.d.ts +11 -2
  29. package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
  30. package/dist/types/index.d.ts +1 -1
  31. package/dist/types/types/chart.d.ts +8 -1
  32. package/dist/types/types/index.d.ts +1 -1
  33. package/dist/types/utils/formatters.d.ts +4 -4
  34. package/dist/types/utils/xAxisNumber.d.ts +2 -0
  35. package/package.json +1 -1
  36. package/src/App.vue +34 -100
  37. package/src/components/WaveformChartView.vue +0 -4
  38. package/src/components/animation/config.ts +1 -1
  39. package/src/components/animation/context.ts +9 -1
  40. package/src/components/animation/paginationPresentation.ts +55 -64
  41. package/src/components/animation/useWaveformAnimations.ts +36 -13
  42. package/src/components/controls/useChartCommands.ts +12 -13
  43. package/src/components/core/amplitudeLimits.ts +44 -0
  44. package/src/components/core/layout.ts +22 -34
  45. package/src/components/core/trackLayoutBuilder.ts +12 -5
  46. package/src/components/core/useWaveformChartController.ts +3 -4
  47. package/src/components/core/useWaveformChartLifecycle.ts +11 -2
  48. package/src/components/core/useWaveformData.ts +11 -5
  49. package/src/components/core/useWaveformLayout.ts +9 -4
  50. package/src/components/core/useWaveformMeasurements.ts +3 -0
  51. package/src/components/core/useWaveformYAxisLayout.ts +2 -0
  52. package/src/components/core/waveformChartTypes.ts +4 -1
  53. package/src/components/core/xAxisTicks.ts +69 -0
  54. package/src/components/core/yAxisLayoutMetrics.ts +4 -0
  55. package/src/components/core/yAxisTicks.ts +39 -0
  56. package/src/components/data/types.ts +2 -0
  57. package/src/components/interaction/integerZoom.ts +31 -12
  58. package/src/components/interaction/remoteViewport.ts +26 -4
  59. package/src/components/interaction/stepZoom.ts +33 -0
  60. package/src/components/interaction/useWaveformViewport.ts +3 -26
  61. package/src/components/interaction/useWaveformZoom.ts +50 -157
  62. package/src/components/interaction/zoomConstraints.ts +5 -5
  63. package/src/components/rendering/WaveformSeriesLayer.vue +1 -2
  64. package/src/components/rendering/WaveformTrack.vue +1 -6
  65. package/src/core/data.ts +5 -2
  66. package/src/data/simulatedWaveforms.ts +0 -10
  67. package/src/demo/DemoChartHost.vue +49 -4
  68. package/src/demo/remoteWaveformLoader.ts +114 -0
  69. package/src/demo/types.ts +11 -1
  70. package/src/demo/useRemoteWaveformWindow.ts +229 -0
  71. package/src/index.ts +2 -0
  72. package/src/types/chart.ts +10 -1
  73. package/src/types/index.ts +2 -0
  74. package/src/utils/formatters.ts +10 -5
  75. package/src/utils/xAxisNumber.ts +36 -0
  76. package/src/views/AnimationDemo.vue +14 -5
  77. package/dist/types/data/sx2Channel.d.ts +0 -10
  78. package/src/data/sx2Channel.ts +0 -315
@@ -0,0 +1,229 @@
1
+ import { computed, onScopeDispose, shallowRef, watch, type Ref } from 'vue'
2
+ import type {
3
+ WaveformData,
4
+ WaveformPanEndPayload,
5
+ WaveformZoomEndPayload,
6
+ WaveformZoomIntentPayload,
7
+ WaveformZoomResetPayload,
8
+ } from '../types'
9
+ import {
10
+ createMockWindowLoader,
11
+ mergeWaveformWindow,
12
+ selectWaveformWindow,
13
+ waveformRecordMetadata,
14
+ type RemoteWindowLoader,
15
+ type RemoteWindowRequest,
16
+ } from './remoteWaveformLoader'
17
+
18
+ interface WindowState {
19
+ request: RemoteWindowRequest
20
+ version: number
21
+ controller?: AbortController
22
+ loading: boolean
23
+ error?: string
24
+ }
25
+
26
+ interface RemoteWindowOptions {
27
+ loadWindow?: RemoteWindowLoader
28
+ resetViewport?: (trackIndex?: number) => void
29
+ }
30
+
31
+ type WindowPayload = WaveformPanEndPayload | WaveformZoomEndPayload
32
+
33
+ export function useRemoteWaveformWindow(
34
+ source: Ref<WaveformData>,
35
+ options: RemoteWindowOptions = {},
36
+ ) {
37
+ const metadata = computed(() => waveformRecordMetadata(source.value))
38
+ const recordDomain = computed(() => metadata.value.domain)
39
+ const initialXDomain = computed(() => recordDomain.value)
40
+ const minZoomSpan = computed(() => metadata.value.minZoomSpan)
41
+ const chartData = shallowRef<WaveformData>(
42
+ selectWaveformWindow(
43
+ source.value,
44
+ { start: recordDomain.value[0], end: recordDomain.value[1] },
45
+ true,
46
+ ),
47
+ )
48
+ const states = shallowRef(new Map<string, WindowState>())
49
+ const loadWindow = options.loadWindow ?? createMockWindowLoader(() => source.value)
50
+ let disposed = false
51
+ let intentTarget: string | undefined
52
+ let intentSequence = 0
53
+ const pendingCommands = new Map<string, object>()
54
+
55
+ const keyFor = (seriesIds?: string[]) =>
56
+ seriesIds?.length ? JSON.stringify([...seriesIds].sort()) : 'shared'
57
+ const publish = () => (states.value = new Map(states.value))
58
+
59
+ function cancel(key: string) {
60
+ const state = states.value.get(key)
61
+ if (!state) return
62
+ state.version++
63
+ state.controller?.abort()
64
+ state.controller = undefined
65
+ state.loading = false
66
+ state.error = undefined
67
+ }
68
+
69
+ function cancelAll() {
70
+ for (const key of states.value.keys()) cancel(key)
71
+ publish()
72
+ }
73
+
74
+ function handleZoomIntent(payload: WaveformZoomIntentPayload) {
75
+ const key = keyFor(payload.seriesIds)
76
+ if (key === 'shared') pendingCommands.clear()
77
+ else pendingCommands.delete(key)
78
+ if (key === 'shared') cancelAll()
79
+ else {
80
+ cancel('shared')
81
+ cancel(key)
82
+ }
83
+ const previous = states.value.get(key)
84
+ states.value.set(key, {
85
+ request: { start: payload.start, end: payload.end, seriesIds: payload.seriesIds },
86
+ version: previous?.version ?? 0,
87
+ loading: false,
88
+ })
89
+ publish()
90
+ // Command intent/change/end arrive synchronously per target. Scope their
91
+ // changes so the next target cannot cancel a valid request in the same batch.
92
+ intentTarget = key
93
+ const sequence = ++intentSequence
94
+ queueMicrotask(() => {
95
+ if (sequence === intentSequence) intentTarget = undefined
96
+ })
97
+ }
98
+
99
+ function handleZoomChange(domain: [number, number]) {
100
+ if (intentTarget) {
101
+ cancel(intentTarget)
102
+ publish()
103
+ } else {
104
+ cancelAll()
105
+ // This event has no target metadata. It must never start an unscoped
106
+ // request in independent mode; pan-end supplies the final target.
107
+ for (const state of states.value.values()) {
108
+ state.request = { ...state.request, start: domain[0], end: domain[1] }
109
+ }
110
+ }
111
+ }
112
+
113
+ async function load(payload: WindowPayload) {
114
+ if (disposed) return
115
+ const key = keyFor(payload.seriesIds)
116
+ if (key === 'shared') cancelAll()
117
+ else cancel('shared')
118
+ cancel(key)
119
+ const controller = new AbortController()
120
+ const state: WindowState = {
121
+ request: {
122
+ start: payload.start,
123
+ end: payload.end,
124
+ ...(payload.seriesIds?.length ? { seriesIds: [...payload.seriesIds] } : {}),
125
+ },
126
+ version: (states.value.get(key)?.version ?? 0) + 1,
127
+ controller,
128
+ loading: true,
129
+ }
130
+ const version = state.version
131
+ const record = source.value
132
+ states.value.set(key, state)
133
+ publish()
134
+ const current = () =>
135
+ !disposed &&
136
+ source.value === record &&
137
+ !controller.signal.aborted &&
138
+ states.value.get(key) === state &&
139
+ state.version === version
140
+ try {
141
+ const response = await loadWindow(state.request, controller.signal)
142
+ if (!current()) return
143
+ chartData.value = mergeWaveformWindow(chartData.value, response, state.request.seriesIds)
144
+ } catch (error) {
145
+ if (!current()) return
146
+ state.error = error instanceof Error ? error.message : '数据加载失败'
147
+ } finally {
148
+ if (current()) {
149
+ state.loading = false
150
+ state.controller = undefined
151
+ publish()
152
+ }
153
+ }
154
+ }
155
+
156
+ function resetWaveformViewport(payload?: WaveformZoomResetPayload) {
157
+ pendingCommands.clear()
158
+ if (!payload?.seriesIds?.length) {
159
+ cancelAll()
160
+ states.value = new Map()
161
+ }
162
+ options.resetViewport?.(payload?.trackIndex)
163
+ return load({
164
+ start: initialXDomain.value[0],
165
+ end: initialXDomain.value[1],
166
+ seriesIds: payload?.seriesIds,
167
+ })
168
+ }
169
+
170
+ function retry() {
171
+ return Promise.all(
172
+ [...states.value.values()].filter((state) => state.error).map((state) => load(state.request)),
173
+ )
174
+ }
175
+
176
+ async function handleZoomEnd(payload: WaveformZoomEndPayload) {
177
+ if (!payload.commandId) return load(payload)
178
+ const key = keyFor(payload.seriesIds)
179
+ const pending = {}
180
+ const record = source.value
181
+ pendingCommands.set(key, pending)
182
+ // Collect the synchronous command batch before starting requests. Fit has
183
+ // no intent event, so its unscoped changes would otherwise cancel siblings.
184
+ await Promise.resolve()
185
+ if (disposed || source.value !== record || pendingCommands.get(key) !== pending) return
186
+ pendingCommands.delete(key)
187
+ return load(payload)
188
+ }
189
+
190
+ function handlePanEnd(payload: WaveformPanEndPayload) {
191
+ pendingCommands.clear()
192
+ return load(payload)
193
+ }
194
+
195
+ watch(source, () => {
196
+ pendingCommands.clear()
197
+ cancelAll()
198
+ states.value = new Map()
199
+ intentTarget = undefined
200
+ chartData.value = selectWaveformWindow(
201
+ source.value,
202
+ { start: recordDomain.value[0], end: recordDomain.value[1] },
203
+ true,
204
+ )
205
+ void resetWaveformViewport()
206
+ })
207
+ onScopeDispose(() => {
208
+ disposed = true
209
+ pendingCommands.clear()
210
+ cancelAll()
211
+ })
212
+
213
+ return {
214
+ chartData,
215
+ recordDomain,
216
+ initialXDomain,
217
+ minZoomSpan,
218
+ loading: computed(() => [...states.value.values()].some((state) => state.loading)),
219
+ errors: computed(() =>
220
+ [...states.value.values()].flatMap((state) => (state.error ? [state.error] : [])),
221
+ ),
222
+ handleZoomIntent,
223
+ handleZoomChange,
224
+ handleZoomEnd,
225
+ handlePanEnd,
226
+ resetWaveformViewport,
227
+ retry,
228
+ }
229
+ }
package/src/index.ts CHANGED
@@ -62,6 +62,8 @@ export type {
62
62
  // 类型定义
63
63
  export type {
64
64
  // 图表类型
65
+ WaveformAmplitudeLimit,
66
+ WaveformAmplitudeLimits,
65
67
  WaveformPoint,
66
68
  WaveformDisplayMode,
67
69
  WaveformUnitDisplayMode,
@@ -81,7 +81,7 @@ export interface WaveformZoomIntentPayload extends WaveformCommandMetadata {
81
81
  seriesIds?: string[]
82
82
  }
83
83
 
84
- /** Identifies the viewport reset by a double-click gesture. */
84
+ /** Identifies the viewport reset by a toolbar or API command. */
85
85
  export interface WaveformZoomResetPayload extends WaveformCommandMetadata {
86
86
  trackIndex?: number
87
87
  seriesIds?: string[]
@@ -319,3 +319,12 @@ export interface WaveformZeroLineOptions {
319
319
  /** Hide within this fraction of a Y-axis tick interval from either boundary. Defaults to 0.02. */
320
320
  boundaryThreshold?: number
321
321
  }
322
+
323
+ /** Strict amplitude display bounds, in the series' original Y units. */
324
+ export interface WaveformAmplitudeLimit {
325
+ lower?: number
326
+ upper?: number
327
+ }
328
+
329
+ /** Keyed by stable normalized series IDs (not track IDs). */
330
+ export type WaveformAmplitudeLimits = Record<string, WaveformAmplitudeLimit>
@@ -4,6 +4,8 @@
4
4
 
5
5
  // 图表类型
6
6
  export type {
7
+ WaveformAmplitudeLimit,
8
+ WaveformAmplitudeLimits,
7
9
  WaveformPoint,
8
10
  WaveformDisplayMode,
9
11
  WaveformUnitDisplayMode,
@@ -1,4 +1,5 @@
1
1
  import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types'
2
+ import { formatXAxisNumber } from './xAxisNumber'
2
3
 
3
4
  /**
4
5
  * 时间单位类型
@@ -147,7 +148,11 @@ export function formatXAxisLabel(
147
148
  formatter?: WaveformXAxisLabelFormatter,
148
149
  ): string {
149
150
  const value = displayTime(rawValue, timeUnit)
150
- if (!formatter) return formatPlainNumber(value)
151
+ if (!formatter)
152
+ return formatXAxisNumber(value, [
153
+ displayTime(domain[0], timeUnit),
154
+ displayTime(domain[1], timeUnit),
155
+ ])
151
156
 
152
157
  return formatter(value, {
153
158
  kind,
@@ -173,9 +178,9 @@ export function endpointFractionDigits(domain: [number, number], timeUnit: TimeU
173
178
  }
174
179
 
175
180
  /**
176
- * 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
181
+ * 将 X 轴端点时间格式化为当前显示单位下的自适应精度文本
177
182
  * @param value 时间值(秒)
178
- * @param _domain 数据域(为保持现有调用签名而保留)
183
+ * @param _domain 用于计算显示精度的可视域
179
184
  * @param timeUnit 时间单位
180
185
  * @returns 格式化的时间字符串
181
186
  */
@@ -188,10 +193,10 @@ export function formatEndpointTime(
188
193
  }
189
194
 
190
195
  /**
191
- * 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
196
+ * 将 X 轴时间刻度格式化为当前显示单位下的自适应精度文本
192
197
  * @param value 时间值(秒)
193
198
  * @param timeUnit 时间单位
194
- * @param _domain 数据域(为保持现有调用签名而保留)
199
+ * @param _domain 用于计算显示精度的可视域
195
200
  * @returns 格式化的时间字符串
196
201
  */
197
202
  export function formatAxisTime(
@@ -0,0 +1,36 @@
1
+ /** Compact labels share precision so even extremely close endpoints stay distinct. */
2
+ export function formatXAxisNumber(value: number, domain: [number, number]): string {
3
+ if (!Number.isFinite(value)) return String(value)
4
+ const span = Math.abs(domain[1] - domain[0])
5
+ const digits =
6
+ Number.isFinite(span) && span > 0 ? Math.max(0, Math.ceil(-Math.log10(span)) + 1) : 0
7
+ const clean = (number: number) => Number(number.toPrecision(15))
8
+ const fractionalDigits = (number: number) => {
9
+ const text = String(clean(number))
10
+ return text.includes('e') ? 6 : (text.split('.')[1]?.length ?? 0)
11
+ }
12
+ // Wide windows can still have fractional boundaries; do not label 2.76 as 3.
13
+ const precision =
14
+ span >= 1 || span === 0 ? Math.max(digits, Math.min(6, fractionalDigits(value))) : digits
15
+ const fixed = (number: number) => {
16
+ const rounded = Number(clean(number).toFixed(Math.min(6, precision)))
17
+ return Math.abs(rounded) >= 1e21
18
+ ? rounded.toLocaleString('en-US', { useGrouping: false, maximumFractionDigits: 6 })
19
+ : String(rounded)
20
+ }
21
+ if (
22
+ precision <= 6 &&
23
+ (value === 0 || Number(fixed(value)) !== 0) &&
24
+ (span === 0 || fixed(domain[0]) !== fixed(domain[1]))
25
+ ) {
26
+ return fixed(value)
27
+ }
28
+ for (let significant = 2; significant <= 17; significant++) {
29
+ const scientific = (number: number) => {
30
+ const [mantissa, exponent] = number.toExponential(significant - 1).split('e')
31
+ return `${Number(mantissa)}e${exponent}`
32
+ }
33
+ if (span === 0 || scientific(domain[0]) !== scientific(domain[1])) return scientific(value)
34
+ }
35
+ return value.toExponential(16)
36
+ }
@@ -13,6 +13,7 @@ const enter = ref(true)
13
13
  const update = ref(true)
14
14
  const annotationEnabled = ref(true)
15
15
  const effect = ref<AnnotationAnimationType>('scale')
16
+ const gridShape = ref('1x1')
16
17
  const direction = ref<LineDrawingDirection>('left-to-right')
17
18
  let generation = 0
18
19
  function generate(count = 500): WaveformData {
@@ -55,13 +56,13 @@ function toggleAnnotation() {
55
56
  <main class="animation-demo">
56
57
  <h1>波形动画</h1>
57
58
  <p>
58
- 分别控制入场、更新 /
59
- 切页和标注效果。切换分页时波形与标注同步淡入,坐标轴立即更新。拖动、缩放与导出会提交最终画面。
59
+ 分别控制入场、更新和标注效果。分页与网格调整重播波形绘制,标注等待对应系列完成,
60
+ 坐标轴立即更新。拖动、缩放与导出会提交最终画面。
60
61
  </p>
61
62
  <div class="animation-demo__controls">
62
63
  <label><input v-model="enabled" type="checkbox" /> 动画</label>
63
- <label><input v-model="enter" type="checkbox" /> 入场</label>
64
- <label><input v-model="update" type="checkbox" /> 更新 / 切页</label>
64
+ <label><input v-model="enter" type="checkbox" /> 入场 / 切页 / 网格</label>
65
+ <label><input v-model="update" type="checkbox" /> 更新</label>
65
66
  <label><input v-model="annotationEnabled" type="checkbox" /> 标注</label>
66
67
  <label
67
68
  >入场方向
@@ -80,6 +81,14 @@ function toggleAnnotation() {
80
81
  <option value="bounce">弹跳</option>
81
82
  </select></label
82
83
  >
84
+ <label
85
+ >网格
86
+ <select v-model="gridShape" aria-label="网格">
87
+ <option value="1x1">1×1</option>
88
+ <option value="1x2">1×2</option>
89
+ <option value="2x2">2×2</option>
90
+ </select>
91
+ </label>
83
92
  <button @click="key++">重新入场</button>
84
93
  <button @click="replace(true)">更新振幅</button>
85
94
  <button @click="replace(false)">改变点数</button>
@@ -89,7 +98,7 @@ function toggleAnnotation() {
89
98
  :key="key"
90
99
  :data="data"
91
100
  :height="360"
92
- :grid="{ rowCount: 1, columnCount: 1 }"
101
+ :grid="{ rowCount: Number(gridShape[0]), columnCount: Number(gridShape[2]) }"
93
102
  :animation-enabled="enabled"
94
103
  :animation="{ enabled: enter, direction }"
95
104
  :animation-update="{ enabled: update }"
@@ -1,10 +0,0 @@
1
- export declare const sx2Channel: {
2
- chnl: string;
3
- chnl_id: number;
4
- dat_unit: string;
5
- dev: number;
6
- shot: number;
7
- time_unit: string;
8
- data: number[];
9
- time: number[];
10
- };