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,23 @@
1
+ import type { InjectionKey } from 'vue';
2
+ import type { WaveformData } from '../types';
3
+ export interface RemoteWindowRequest {
4
+ start: number;
5
+ end: number;
6
+ seriesIds?: string[];
7
+ }
8
+ export type RemoteWindowLoader = (request: RemoteWindowRequest, signal: AbortSignal) => Promise<WaveformData>;
9
+ export declare const remoteWindowLoaderKey: InjectionKey<RemoteWindowLoader>;
10
+ export interface MockWindowOptions {
11
+ delayMs?: number | ((request: RemoteWindowRequest) => number);
12
+ empty?: boolean | ((request: RemoteWindowRequest) => boolean);
13
+ fail?: boolean | ((request: RemoteWindowRequest) => boolean);
14
+ }
15
+ /** Preserve series identities even when a window has no samples. */
16
+ export declare function selectWaveformWindow(data: WaveformData, request: RemoteWindowRequest, empty?: boolean): WaveformData;
17
+ export declare function mergeWaveformWindow(current: WaveformData, response: WaveformData, seriesIds?: string[]): WaveformData;
18
+ /** Metadata is derived from the complete record, never from a loaded window. */
19
+ export declare function waveformRecordMetadata(data: WaveformData): {
20
+ domain: [number, number];
21
+ minZoomSpan: number | undefined;
22
+ };
23
+ export declare function createMockWindowLoader(source: () => WaveformData, options?: MockWindowOptions): RemoteWindowLoader;
@@ -1,4 +1,5 @@
1
- import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformPlotMargin, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
1
+ import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformPlotMargin, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload, WaveformZoomIntentPayload } from '../components';
2
+ import type { WaveformZoomResetPayload, WaveformPanEndPayload } from '../types';
2
3
  import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls';
3
4
  interface SelectOption<T> {
4
5
  label: string;
@@ -78,6 +79,11 @@ export interface DemoChartModel {
78
79
  toolbarVisible: boolean;
79
80
  data: WaveformData;
80
81
  initialXDomain?: [number, number];
82
+ recordDomain: [number, number];
83
+ minZoomSpan?: number;
84
+ loading: boolean;
85
+ errors: string[];
86
+ retry: () => Promise<void[]>;
81
87
  displayMode: WaveformDisplayMode;
82
88
  overlayMode: WaveformOverlayMode;
83
89
  rowCount: number;
@@ -101,6 +107,9 @@ export interface DemoChartModel {
101
107
  interactionMode: WaveformInteractionMode;
102
108
  hiddenSeriesIds: string[];
103
109
  handleZoomEnd: (payload: WaveformZoomEndPayload) => void | Promise<void>;
104
- resetWaveformViewport: () => void;
110
+ handleZoomIntent: (payload: WaveformZoomIntentPayload) => void;
111
+ handleZoomChange: (domain: [number, number]) => void;
112
+ handlePanEnd: (payload: WaveformPanEndPayload) => void | Promise<void>;
113
+ resetWaveformViewport: (payload?: WaveformZoomResetPayload) => void | Promise<void>;
105
114
  }
106
115
  export {};
@@ -0,0 +1,22 @@
1
+ import { type Ref } from 'vue';
2
+ import type { WaveformData, WaveformPanEndPayload, WaveformZoomEndPayload, WaveformZoomIntentPayload, WaveformZoomResetPayload } from '../types';
3
+ import { type RemoteWindowLoader } from './remoteWaveformLoader';
4
+ interface RemoteWindowOptions {
5
+ loadWindow?: RemoteWindowLoader;
6
+ resetViewport?: (trackIndex?: number) => void;
7
+ }
8
+ export declare function useRemoteWaveformWindow(source: Ref<WaveformData>, options?: RemoteWindowOptions): {
9
+ chartData: import("vue").ShallowRef<WaveformData, WaveformData>;
10
+ recordDomain: import("vue").ComputedRef<[number, number]>;
11
+ initialXDomain: import("vue").ComputedRef<[number, number]>;
12
+ minZoomSpan: import("vue").ComputedRef<number | undefined>;
13
+ loading: import("vue").ComputedRef<boolean>;
14
+ errors: import("vue").ComputedRef<string[]>;
15
+ handleZoomIntent: (payload: WaveformZoomIntentPayload) => void;
16
+ handleZoomChange: (domain: [number, number]) => void;
17
+ handleZoomEnd: (payload: WaveformZoomEndPayload) => Promise<void>;
18
+ handlePanEnd: (payload: WaveformPanEndPayload) => Promise<void>;
19
+ resetWaveformViewport: (payload?: WaveformZoomResetPayload) => Promise<void>;
20
+ retry: () => Promise<void[]>;
21
+ };
22
+ export {};
@@ -13,7 +13,7 @@ export { Animation } from './animation/core/Animation';
13
13
  export { Animator, globalAnimator } from './animation/core/Animator';
14
14
  export type { AnimationValue, AnimationDirection, AnimationState, AnimationFrame, AnimationTrack, AnimationConfig, AnimationHandle, AnimationScheduler, EasingFunction, EasingFamily, EasingType, } from './animation/core/types';
15
15
  export type { LineDrawingDirection, AnnotationAnimationType, LineDrawingOptions, StaggeredLineDrawingOptions, DataUpdateOptions, AnnotationAnimationOptions, WaveformEnterAnimationOptions, WaveformUpdateAnimationOptions, WaveformAnnotationAnimationOptions, WaveformAnimationProps, ClipAnimationState, AnimationBinding, NumericGeometry, GeometryTransitionInput, GeometryTransitionFrame, AnnotationAnimationState, AnimationScope, AnimationObjectKey, WaveformAnimationControllerOptions, } from './animation/types';
16
- export type { WaveformPoint, WaveformDisplayMode, WaveformUnitDisplayMode, WaveformLayoutPreset, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformPanEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformYAxisLabelTextStyle, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformTypedValues, TypedSampleData, TypedPointData, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
16
+ export type { WaveformAmplitudeLimit, WaveformAmplitudeLimits, WaveformPoint, WaveformDisplayMode, WaveformUnitDisplayMode, WaveformLayoutPreset, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformPanEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformYAxisLabelTextStyle, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformTypedValues, TypedSampleData, TypedPointData, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
17
17
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './components/core/grid';
18
18
  export { normalizeWaveformData, normalizeWaveformSeries } from './core';
19
19
  export { paddedDomain, buildMinorTicks, displayTime, formatEndpointTime, formatAxisTime, formatTooltipTime, resolveTrackGeometry, clamp, type TimeUnit, type TrackGeometry, } from './utils';
@@ -68,7 +68,7 @@ export interface WaveformZoomIntentPayload extends WaveformCommandMetadata {
68
68
  trackIndex?: number;
69
69
  seriesIds?: string[];
70
70
  }
71
- /** Identifies the viewport reset by a double-click gesture. */
71
+ /** Identifies the viewport reset by a toolbar or API command. */
72
72
  export interface WaveformZoomResetPayload extends WaveformCommandMetadata {
73
73
  trackIndex?: number;
74
74
  seriesIds?: string[];
@@ -279,3 +279,10 @@ export interface WaveformZeroLineOptions {
279
279
  /** Hide within this fraction of a Y-axis tick interval from either boundary. Defaults to 0.02. */
280
280
  boundaryThreshold?: number;
281
281
  }
282
+ /** Strict amplitude display bounds, in the series' original Y units. */
283
+ export interface WaveformAmplitudeLimit {
284
+ lower?: number;
285
+ upper?: number;
286
+ }
287
+ /** Keyed by stable normalized series IDs (not track IDs). */
288
+ export type WaveformAmplitudeLimits = Record<string, WaveformAmplitudeLimit>;
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformUnitDisplayMode, WaveformLayoutPreset, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformPanEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformYAxisLabelTextStyle, WaveformZeroLineOptions, } from './chart';
4
+ export type { WaveformAmplitudeLimit, WaveformAmplitudeLimits, WaveformPoint, WaveformDisplayMode, WaveformUnitDisplayMode, WaveformLayoutPreset, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformPanEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformYAxisLabelTextStyle, WaveformZeroLineOptions, } from './chart';
5
5
  export type { SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformTypedValues, TypedSampleData, TypedPointData, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './data';
6
6
  export type { WaveformToolbarItem, WaveformToolbarOptions, WaveformControlTarget, WaveformControlAction, WaveformCommandMetadata, WaveformTargetStatus, WaveformViewportSnapshot, WaveformTargetResult, WaveformControlResult, WaveformControlState, WaveformImageExportOptions, WaveformImageExportErrorCode, WaveformImageExportError, WaveformChartHandle, } from './controls';
@@ -46,18 +46,18 @@ export declare function formatXAxisLabel(rawValue: number, domain: [number, numb
46
46
  */
47
47
  export declare function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number;
48
48
  /**
49
- * 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
49
+ * 将 X 轴端点时间格式化为当前显示单位下的自适应精度文本
50
50
  * @param value 时间值(秒)
51
- * @param _domain 数据域(为保持现有调用签名而保留)
51
+ * @param _domain 用于计算显示精度的可视域
52
52
  * @param timeUnit 时间单位
53
53
  * @returns 格式化的时间字符串
54
54
  */
55
55
  export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
56
56
  /**
57
- * 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
57
+ * 将 X 轴时间刻度格式化为当前显示单位下的自适应精度文本
58
58
  * @param value 时间值(秒)
59
59
  * @param timeUnit 时间单位
60
- * @param _domain 数据域(为保持现有调用签名而保留)
60
+ * @param _domain 用于计算显示精度的可视域
61
61
  * @returns 格式化的时间字符串
62
62
  */
63
63
  export declare function formatAxisTime(value: number, timeUnit: TimeUnit, _domain?: [number, number]): string;
@@ -0,0 +1,2 @@
1
+ /** Compact labels share precision so even extremely close endpoints stay distinct. */
2
+ export declare function formatXAxisNumber(value: number, domain: [number, number]): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.vue CHANGED
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
2
+ import { computed, inject, onBeforeUnmount, onMounted, reactive, ref, shallowRef } from 'vue'
3
3
  import 'vue3-colorpicker/style.css'
4
4
 
5
5
  import {
@@ -16,7 +16,6 @@ import {
16
16
  type WaveformOverlayMode,
17
17
  type WaveformPlotMargin,
18
18
  type WaveformTitleOptions,
19
- type WaveformZoomEndPayload,
20
19
  type WaveformZeroLineOptions,
21
20
  } from './components'
22
21
  import { normalizeWaveformSeries } from './core'
@@ -25,6 +24,8 @@ import DemoChartHost from './demo/DemoChartHost.vue'
25
24
  import DemoControlPanel from './demo/DemoControlPanel.vue'
26
25
  import type { DemoChartModel, DemoControlPanelModel } from './demo/types'
27
26
  import { useDemoXAxisLabelControls } from './demo/useDemoXAxisLabelControls'
27
+ import { remoteWindowLoaderKey } from './demo/remoteWaveformLoader'
28
+ import { useRemoteWaveformWindow } from './demo/useRemoteWaveformWindow'
28
29
 
29
30
  const demoPreset = new URLSearchParams(window.location.search).get('demo')
30
31
  const compactNineDemo = demoPreset === 'compact-nine'
@@ -164,20 +165,27 @@ const plotMargin = computed<WaveformPlotMargin>(() => ({
164
165
  bottom: plotMarginBottom.value,
165
166
  }))
166
167
 
167
- const initialXValues = normalizeWaveformSeries(fullChartData).flatMap((series) =>
168
- series.points.map((point) => point.x),
169
- )
170
- const [initialXMinimum, initialXMaximum] = initialXValues.reduce<[number, number]>(
171
- ([minimum, maximum], value) => [Math.min(minimum, value), Math.max(maximum, value)],
172
- [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY],
173
- )
174
- const initialXDomainValue: [number, number] | undefined =
175
- Number.isFinite(initialXMinimum) && Number.isFinite(initialXMaximum)
176
- ? [initialXMinimum, initialXMaximum]
177
- : undefined
178
- // Keep the full source domain stable while viewport data windows are replaced.
179
- const initialXDomain = ref<[number, number] | undefined>(initialXDomainValue)
180
- const chartData = ref<WaveformData>(fullChartData)
168
+ const demoChartHostRef = ref<{ resetViewport: (trackIndex?: number) => void }>()
169
+ const sourceData = shallowRef<WaveformData>(fullChartData)
170
+ const remoteWindow = useRemoteWaveformWindow(sourceData, {
171
+ loadWindow: inject(remoteWindowLoaderKey, undefined),
172
+ resetViewport: (trackIndex) => demoChartHostRef.value?.resetViewport(trackIndex),
173
+ })
174
+ const {
175
+ chartData,
176
+ initialXDomain,
177
+ recordDomain,
178
+ minZoomSpan,
179
+ loading,
180
+ errors,
181
+ handleZoomIntent,
182
+ handleZoomChange,
183
+ handleZoomEnd,
184
+ handlePanEnd,
185
+ resetWaveformViewport,
186
+ retry,
187
+ } = remoteWindow
188
+ onMounted(() => void resetWaveformViewport())
181
189
  const lineStyleOverrides = ref<Record<string, WaveformLineStyle>>({})
182
190
  const selectedSeriesId = ref(
183
191
  fullChartData.kind === 'series' ? (fullChartData.series[0]?.id ?? '') : '',
@@ -188,7 +196,7 @@ const lineStyleOptions: Array<{ label: string; value: WaveformLineStyle }> = [
188
196
  { label: '点划线', value: 'dash-dot' },
189
197
  ]
190
198
  const seriesStyleOptions = computed(() =>
191
- normalizeWaveformSeries(chartData.value).map((series) => ({
199
+ normalizeWaveformSeries(sourceData.value).map((series) => ({
192
200
  label: series.name || series.id,
193
201
  value: series.id,
194
202
  })),
@@ -214,7 +222,7 @@ const displayChartData = computed<WaveformData>(() => {
214
222
  })
215
223
  const gridTrackLines = computed<WaveformGridTrackLines>(() =>
216
224
  Object.fromEntries(
217
- normalizeWaveformSeries(displayChartData.value).map((series) => [
225
+ normalizeWaveformSeries(sourceData.value).map((series) => [
218
226
  series.trackId ?? series.id,
219
227
  {
220
228
  horizontal: horizontalGridVisible.value,
@@ -225,88 +233,6 @@ const gridTrackLines = computed<WaveformGridTrackLines>(() =>
225
233
  ]),
226
234
  ),
227
235
  )
228
- const demoChartHostRef = ref<{ resetViewport: (trackIndex?: number) => void }>()
229
-
230
- let zoomRequestSequence = 0
231
-
232
- function filterWaveformData(data: WaveformData, start: number, end: number): WaveformData {
233
- const lower = Math.min(start, end)
234
- const upper = Math.max(start, end)
235
- if (data.kind !== 'series') {
236
- if (data.kind !== 'points') return data
237
- return {
238
- kind: 'points',
239
- points: data.points.filter((point) => point.x >= lower && point.x <= upper),
240
- }
241
- }
242
- return {
243
- kind: 'series',
244
- series: data.series.map((series) => ({
245
- ...series,
246
- data:
247
- series.data.kind === 'points'
248
- ? {
249
- kind: 'points',
250
- points: series.data.points.filter((point) => point.x >= lower && point.x <= upper),
251
- }
252
- : series.data,
253
- })),
254
- }
255
- }
256
-
257
- function mergeIndependentWindow(
258
- currentData: WaveformData,
259
- responseData: WaveformData,
260
- seriesIds: string[],
261
- ): WaveformData {
262
- if (currentData.kind !== 'series' || responseData.kind !== 'series') return responseData
263
- const responseById = new Map(responseData.series.map((series) => [series.id, series]))
264
- const changedIds = new Set(seriesIds)
265
- return {
266
- kind: 'series',
267
- series: currentData.series.map((series) =>
268
- series.id && changedIds.has(series.id) ? (responseById.get(series.id) ?? series) : series,
269
- ),
270
- }
271
- }
272
-
273
- let pendingCommandId: string | undefined
274
- let pendingPayloads: WaveformZoomEndPayload[] = []
275
- async function handleZoomEnd(payload: WaveformZoomEndPayload) {
276
- if (payload.action === 'fit') return
277
- if (payload.commandId && payload.commandId === pendingCommandId) {
278
- pendingPayloads.push(payload)
279
- return
280
- }
281
- pendingCommandId = payload.commandId
282
- const batch = [payload]
283
- pendingPayloads = batch
284
- // Demo-only sequence number cancellation. Production code should use AbortController
285
- // to cancel in-flight requests when a newer zoom gesture arrives.
286
- const requestSequence = ++zoomRequestSequence
287
- await new Promise((resolve) => window.setTimeout(resolve, 80))
288
- if (requestSequence !== zoomRequestSequence) return
289
-
290
- // Demo-only stand-in for the backend response. Production code should replace this
291
- // with a request using payload.start/payload.end and the optional channel metadata.
292
- let nextData = chartData.value
293
- batch.forEach((item) => {
294
- const responseData = filterWaveformData(fullChartData, item.start, item.end)
295
- nextData =
296
- item.trackIndex !== undefined && item.seriesIds?.length
297
- ? mergeIndependentWindow(nextData, responseData, item.seriesIds)
298
- : responseData
299
- })
300
- chartData.value = nextData
301
- pendingCommandId = undefined
302
- }
303
-
304
- function resetWaveformViewport() {
305
- zoomRequestSequence += 1
306
- chartData.value = fullChartData
307
- initialXDomain.value = initialXDomainValue
308
- demoChartHostRef.value?.resetViewport()
309
- }
310
236
  const titleOptions = computed<WaveformTitleOptions>(() => ({
311
237
  visible: titleVisible.value,
312
238
  text: titleText.value,
@@ -406,6 +332,11 @@ const chartModel = reactive({
406
332
  toolbarVisible,
407
333
  data: displayChartData,
408
334
  initialXDomain,
335
+ recordDomain,
336
+ minZoomSpan,
337
+ loading,
338
+ errors,
339
+ retry,
409
340
  displayMode,
410
341
  overlayMode,
411
342
  rowCount,
@@ -428,7 +359,10 @@ const chartModel = reactive({
428
359
  annotationsVisible,
429
360
  interactionMode,
430
361
  hiddenSeriesIds,
362
+ handleZoomIntent,
363
+ handleZoomChange,
431
364
  handleZoomEnd,
365
+ handlePanEnd,
432
366
  resetWaveformViewport,
433
367
  }) satisfies DemoChartModel
434
368
  </script>
@@ -46,11 +46,9 @@ const {
46
46
  chartWidth,
47
47
  drawingHeight,
48
48
  hasWaveformData,
49
- requestViewportReset,
50
49
  trackLayouts,
51
50
  presentationTracks,
52
51
  presentationAnnotations,
53
- paginationOpacity,
54
52
  stabilizeAnimations,
55
53
  clipPathId,
56
54
  chartTopMargin,
@@ -189,7 +187,6 @@ function handleChartPointerLeave() {
189
187
  :height="drawingHeight"
190
188
  role="img"
191
189
  :aria-label="hasWaveformData ? '波形折线图' : '暂无波形数据'"
192
- @dblclick="requestViewportReset"
193
190
  >
194
191
  <defs>
195
192
  <clipPath
@@ -244,7 +241,6 @@ function handleChartPointerLeave() {
244
241
  v-for="track in trackLayouts"
245
242
  :key="`${track.index}-${track.id}`"
246
243
  :track="presentationTracks.find((item) => item.id === track.id) ?? track"
247
- :pagination-opacity="paginationOpacity"
248
244
  :frame-path="
249
245
  displayMode === 'compact'
250
246
  ? compactFramePath(track, trackLayouts, compactFrameInset)
@@ -62,7 +62,7 @@ export function resolveAnimationProps(
62
62
  animationEnabled: field(props.animationEnabled, true, boolean, 'animationEnabled'),
63
63
  animationThreshold: field(
64
64
  props.animationThreshold,
65
- 50000,
65
+ 200000,
66
66
  (v) => nonnegative(v) && Number.isInteger(v),
67
67
  'animationThreshold',
68
68
  ),
@@ -1,5 +1,6 @@
1
1
  import type { ComputedRef, Ref, ShallowRef } from 'vue'
2
2
  import type { TrackLayout } from '../core/types'
3
+ import { normalizeGridOptions } from '../core/grid'
3
4
  import type { PreparedWaveformSeries } from '../core/useWaveformData'
4
5
  import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
5
6
 
@@ -18,6 +19,11 @@ export interface AnimationContext {
18
19
  }
19
20
  }
20
21
 
22
+ export function animationGridShape(context: AnimationContext) {
23
+ const grid = normalizeGridOptions(context.props.grid)
24
+ return `${grid.rowCount}:${grid.columnCount}`
25
+ }
26
+
21
27
  export function animationLayoutSignature(context: AnimationContext) {
22
28
  const { props, tracks } = context
23
29
  return JSON.stringify({
@@ -27,8 +33,10 @@ export function animationLayoutSignature(context: AnimationContext) {
27
33
  track.top,
28
34
  track.width,
29
35
  track.height,
36
+ track.xScale.domain(),
37
+ track.yScale.domain(),
30
38
  track.seriesList.map((series) => series.id),
31
- track.yAxes.map((axis) => axis.seriesList.map((series) => series.id)),
39
+ track.yAxes.map((axis) => [axis.seriesList.map((series) => series.id), axis.scale.domain()]),
32
40
  ]),
33
41
  width: context.innerWidth.value,
34
42
  height: context.innerHeight.value,
@@ -1,22 +1,27 @@
1
- import { shallowRef, type ComputedRef } from 'vue'
2
- import type { AnimationContext } from './context'
1
+ import type { ComputedRef } from 'vue'
2
+ import { animationLayoutSignature, type AnimationContext } from './context'
3
3
  import type { ResolvedAnimationProps } from './config'
4
4
  import type { WaveformAnimationController } from '@/animation/WaveformAnimationController'
5
- import { clampProgress } from '@/animation/presets/shared'
6
5
  import { chartExportOptions } from '../controls/chartExportOptions'
7
6
 
8
- /** A single owner-scoped fade; it holds no old page geometry. */
7
+ /** Coordinates entrance replay; drawing remains owned by the normal series pipeline. */
9
8
  export function createPaginationPresentation(
10
9
  context: AnimationContext,
11
10
  controller: WaveformAnimationController,
12
11
  config: ComputedRef<ResolvedAnimationProps>,
13
12
  effective: ComputedRef<boolean>,
14
- commit: () => void,
13
+ prepare: () => void,
15
14
  ) {
16
- const opacity = shallowRef(1)
17
- const key = { kind: 'custom', id: 'pagination', scope: 'update' } as const
18
- let sequence = 0
19
- let request: { page: number; version: number; environment: string; started: boolean } | undefined
15
+ let request:
16
+ | {
17
+ page: number
18
+ trigger: 'page' | 'grid'
19
+ environment: string
20
+ started: boolean
21
+ geometry?: string
22
+ signature?: string
23
+ }
24
+ | undefined
20
25
  const environment = () =>
21
26
  JSON.stringify({
22
27
  options: Object.fromEntries(
@@ -25,82 +30,58 @@ export function createPaginationPresentation(
25
30
  ),
26
31
  ),
27
32
  size: context.containerSize.value,
33
+ amplitudeLimits: context.props.amplitudeLimits,
28
34
  hidden: context.props.hiddenSeriesIds,
29
35
  presentation: context.props.presentationMode,
30
36
  })
31
37
  function reset() {
32
38
  request = undefined
33
- opacity.value = 1
34
39
  }
35
- function cancel() {
36
- controller.cancel(key)
40
+ function requestPage(page: number, trigger: 'page' | 'grid' = 'page') {
37
41
  reset()
38
- }
39
- function requestPage(page: number) {
40
- cancel()
42
+ controller.cancelAll()
43
+ // Discard old-page presentation before recording the new request.
41
44
  controller.finishAll()
42
- if (!effective.value || !config.value.animationUpdate.enabled) return
43
- request = { page, version: ++sequence, environment: environment(), started: false }
44
- if (config.value.animationUpdate.duration > 0) opacity.value = 0
45
+ if (!effective.value || !config.value.animation.enabled) return
46
+ request = { page, trigger, environment: environment(), started: false }
45
47
  }
46
- function reconcile(dataChanged: boolean) {
48
+ function reconcile(dataChanged: boolean): 'replay' | 'static' | undefined {
47
49
  const current = request
48
- if (!current) return false
50
+ if (!current) return
51
+ if (!config.value.animation.enabled) {
52
+ // Category closure is handled by finishWhere; it must release, not finish annotations.
53
+ reset()
54
+ return
55
+ }
49
56
  if (
50
57
  dataChanged ||
51
58
  current.page !== context.currentPage.value ||
52
59
  current.environment !== environment() ||
53
60
  !effective.value ||
54
- !config.value.animationUpdate.enabled
61
+ (!current.started && config.value.animation.duration === 0) ||
62
+ ['fallback', 'error'].includes(context.sampling.state.value) ||
63
+ (current.started &&
64
+ (current.geometry !== animationLayoutSignature(context) ||
65
+ current.signature !== context.sampling.requestSignature.value ||
66
+ context.sampling.state.value !== 'success'))
55
67
  ) {
56
68
  controller.finishAll()
57
69
  reset()
58
- return true
70
+ return 'static'
59
71
  }
60
- if (!current.started && config.value.animationUpdate.duration === 0) opacity.value = 1
61
- commit()
62
- if (context.sampling.state.value === 'pending' || !context.layoutReady.value) return true
63
- if (context.sampling.state.value !== 'success') {
64
- controller.finishAll()
72
+ if (current.started && !controller.getActiveAnimationCount()) {
65
73
  reset()
66
- return true
74
+ return
67
75
  }
68
- if (current.started) return true
69
- const ids = context.tracks.value.flatMap((track) =>
70
- track.width > 0 && track.height > 0
71
- ? track.seriesList
72
- .filter((series) => (series.source?.length ?? series.points.length) > 0)
73
- .map((series) => series.id)
74
- : [],
75
- )
76
- if (!ids.length) {
77
- reset()
78
- return true
76
+ if (!current.started) {
77
+ prepare()
78
+ current.started = context.sampling.state.value === 'success' && context.layoutReady.value
79
+ if (current.started) {
80
+ current.geometry = animationLayoutSignature(context)
81
+ current.signature = context.sampling.requestSignature.value
82
+ }
79
83
  }
80
- current.started = true
81
- const options = config.value.animationUpdate
82
- const notify = (callback?: () => void) => ids.forEach(() => callback?.())
83
- controller.animate(key, {
84
- duration: options.duration,
85
- easing: options.easing,
86
- force: true,
87
- initialize: () => {
88
- opacity.value = options.duration > 0 ? 0 : 1
89
- },
90
- during: (progress, frame) => {
91
- if (request?.version === current.version)
92
- opacity.value = frame.terminal ? 1 : clampProgress(progress)
93
- },
94
- done: () => {
95
- if (request?.version === current.version) reset()
96
- notify(options.onComplete)
97
- },
98
- aborted: () => {
99
- if (request?.version === current.version) reset()
100
- notify(options.onAbort)
101
- },
102
- })
103
- return true
84
+ return 'replay'
104
85
  }
105
86
  function wrapPagination(goToPage: (page: number) => void, count: ComputedRef<number>) {
106
87
  return (page: number) => {
@@ -110,5 +91,15 @@ export function createPaginationPresentation(
110
91
  goToPage(page)
111
92
  }
112
93
  }
113
- return { opacity, reset, cancel, reconcile, wrapPagination, active: () => !!request }
94
+ return {
95
+ reset,
96
+ reconcile,
97
+ wrapPagination,
98
+ requestLayout: () => requestPage(context.currentPage.value, 'grid'),
99
+ enterDuration: () =>
100
+ request?.trigger === 'page' && config.value.animation.duration > 0
101
+ ? 500
102
+ : config.value.animation.duration,
103
+ active: () => !!request && (!request.started || controller.getActiveAnimationCount() > 0),
104
+ }
114
105
  }