waveform-analysis 0.1.32 → 0.1.35

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.
@@ -44,6 +44,7 @@ declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
44
44
  zoomable: boolean;
45
45
  pannable: boolean;
46
46
  minVisiblePoints: number;
47
+ xDomainStrategy: import("..").WaveformXDomainStrategy;
47
48
  timeUnit: "s" | "ms";
48
49
  annotationsVisible: boolean;
49
50
  grid: import("..").WaveformGridOptions;
@@ -1,6 +1,6 @@
1
1
  import { type ZoomTransform } from 'd3';
2
2
  import { type ResolvedWaveformRenderingOptions } from '../../core/rendering';
3
- import type { WaveformDisplayMode, WaveformOverlayMode, WaveformXAxisLabelFormatter } from '../../types';
3
+ import type { WaveformDisplayMode, WaveformOverlayMode, WaveformXDomainStrategy, WaveformXAxisLabelFormatter } from '../../types';
4
4
  import { type GridCellGeometry, type NormalizedWaveformGridOptions } from './grid';
5
5
  import type { DisplayTrack, TrackLayout } from './types';
6
6
  interface SeriesGridCell extends GridCellGeometry {
@@ -15,6 +15,7 @@ export interface BuildTrackLayoutsOptions {
15
15
  sharedZoomDomain: [number, number];
16
16
  initialXDomain?: [number, number];
17
17
  initialXDomains?: Record<string, [number, number]>;
18
+ xDomainStrategy?: WaveformXDomainStrategy;
18
19
  fixedYDomain?: [number, number];
19
20
  fixedYDomains?: Record<string, [number, number]>;
20
21
  yDomains?: Record<string, [number, number]>;
@@ -330,6 +330,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
330
330
  minZoomSpan?: number | undefined;
331
331
  minVisiblePoints: number;
332
332
  initialXDomains?: Record<string, [number, number]> | undefined;
333
+ xDomainStrategy: import("../..").WaveformXDomainStrategy;
333
334
  yDomain?: [number, number] | undefined;
334
335
  yDomains?: Record<string, [number, number]> | undefined;
335
336
  timeUnit: "s" | "ms";
@@ -1,4 +1,4 @@
1
- import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformInteractionMode, WaveformLegendOptions, WaveformOverlayMode, WaveformPlotMargin, WaveformPoint, WaveformRenderingOptions, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload, WaveformZoomResetPayload } from '../data/types';
1
+ import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformInteractionMode, WaveformLegendOptions, WaveformOverlayMode, WaveformPlotMargin, WaveformPoint, WaveformRenderingOptions, WaveformTitleOptions, WaveformXDomainStrategy, WaveformZeroLineOptions, WaveformZoomEndPayload, WaveformZoomResetPayload } from '../data/types';
2
2
  import type { WaveformGridOptions } from './grid';
3
3
  export interface WaveformChartProps {
4
4
  data: WaveformData;
@@ -16,6 +16,7 @@ export interface WaveformChartProps {
16
16
  minVisiblePoints?: number;
17
17
  initialXDomain?: [number, number];
18
18
  initialXDomains?: Record<string, [number, number]>;
19
+ xDomainStrategy?: WaveformXDomainStrategy;
19
20
  yDomain?: [number, number];
20
21
  yDomains?: Record<string, [number, number]>;
21
22
  timeUnit?: 's' | 'ms';
@@ -36,7 +37,7 @@ export interface WaveformChartProps {
36
37
  presentationMode?: boolean;
37
38
  zeroLine?: WaveformZeroLineOptions;
38
39
  }
39
- type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'pannable' | 'minVisiblePoints' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'plotMargin' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
40
+ type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'pannable' | 'minVisiblePoints' | 'xDomainStrategy' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'plotMargin' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
40
41
  export type ResolvedWaveformChartProps = Readonly<WaveformChartProps & Required<Pick<WaveformChartProps, DefaultedProp>>>;
41
42
  export interface WaveformChartEmit {
42
43
  (event: 'point-hover', point: WaveformPoint | null): void;
@@ -0,0 +1,3 @@
1
+ import type { WaveformXDomainStrategy } from '../../types';
2
+ /** Derives a viewport domain without changing any source coordinates. */
3
+ export declare function applyXDomainStrategy(domain: [number, number], strategy: WaveformXDomainStrategy, explicit?: boolean): [number, number];
@@ -2,6 +2,6 @@
2
2
  * 数据系统类型定义
3
3
  * 重新导出 src/types 中的类型,方便组件内部使用
4
4
  */
5
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from '../../types';
5
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from '../../types';
6
6
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from '../core/grid';
7
7
  export { normalizeWaveformData, normalizeWaveformSeries } from '../../core';
@@ -2,5 +2,5 @@
2
2
  * 向后兼容的导出文件
3
3
  * 保留原有导入路径,内部从新的模块结构导出
4
4
  */
5
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformFrameStyle, SingleWaveformData, WaveformSeries, WaveformLineStyle, WaveformData, NormalizedWaveformSeries, } from '../types';
5
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformFrameStyle, SingleWaveformData, WaveformSeries, WaveformLineStyle, WaveformData, NormalizedWaveformSeries, } from '../types';
6
6
  export { normalizeWaveformData, normalizeWaveformSeries } from '../core';
@@ -3,7 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as WaveformChart } from './components/WaveformChart.vue';
6
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
6
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
7
7
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './components/core/grid';
8
8
  export { normalizeWaveformData, normalizeWaveformSeries } from './core';
9
9
  export { paddedDomain, buildMinorTicks, displayTime, formatEndpointTime, formatAxisTime, formatTooltipTime, resolveTrackGeometry, clamp, type TimeUnit, type TrackGeometry, } from './utils';
@@ -22,6 +22,16 @@ export type WaveformDisplayMode = 'independent' | 'separated' | 'compact';
22
22
  export type WaveformOverlayMode = 'single-axis' | 'multi-axis';
23
23
  /** 标注工具模式 */
24
24
  export type WaveformInteractionMode = 'zoom' | 'annotation';
25
+ /** Controls how the initial X viewport is derived when no explicit domain is configured. */
26
+ export interface WaveformXDomainStrategy {
27
+ type: 'data' | 'nice';
28
+ /** Selects which bounds are expanded when type is `nice`. Defaults to `both`. */
29
+ bounds?: 'both' | 'end';
30
+ /** Stable tick count used to calculate nice bounds. Defaults to 10. */
31
+ tickCount?: number;
32
+ /** Applies the strategy to explicit initial domains. Defaults to false. */
33
+ includeExplicit?: boolean;
34
+ }
25
35
  /** Describes the X-axis viewport after a zoom gesture completes. */
26
36
  export interface WaveformZoomEndPayload {
27
37
  start: number;
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
4
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
5
5
  export type { SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './data';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.32",
3
+ "version": "0.1.35",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -16,6 +16,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
16
16
  zoomable: true,
17
17
  pannable: false,
18
18
  minVisiblePoints: 0,
19
+ xDomainStrategy: () => ({ type: 'data' }),
19
20
  timeUnit: 'ms',
20
21
  frameNumber: undefined,
21
22
  annotations: () => [],
@@ -16,6 +16,7 @@ import type {
16
16
  WaveformDisplayMode,
17
17
  WaveformOverlayMode,
18
18
  WaveformPoint,
19
+ WaveformXDomainStrategy,
19
20
  WaveformXAxisLabelFormatter,
20
21
  } from '../../types'
21
22
  import { buildMinorTicks, formatXAxisLabel } from '../../utils'
@@ -26,6 +27,7 @@ import {
26
27
  } from './grid'
27
28
  import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
28
29
  import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
30
+ import { applyXDomainStrategy } from './xDomain'
29
31
  import {
30
32
  Y_AXIS_LABEL_BAND_WIDTH,
31
33
  Y_AXIS_LABEL_GAP,
@@ -46,6 +48,7 @@ export interface BuildTrackLayoutsOptions {
46
48
  sharedZoomDomain: [number, number]
47
49
  initialXDomain?: [number, number]
48
50
  initialXDomains?: Record<string, [number, number]>
51
+ xDomainStrategy?: WaveformXDomainStrategy
49
52
  fixedYDomain?: [number, number]
50
53
  fixedYDomains?: Record<string, [number, number]>
51
54
  yDomains?: Record<string, [number, number]>
@@ -57,6 +60,21 @@ export interface BuildTrackLayoutsOptions {
57
60
  showCompactEmptyTracks: boolean
58
61
  }
59
62
 
63
+ function resolveIndependentXDomain(
64
+ track: DisplayTrack,
65
+ seriesId: string,
66
+ options: BuildTrackLayoutsOptions,
67
+ ): [number, number] {
68
+ const strategy = options.xDomainStrategy ?? { type: 'data' }
69
+ const explicitDomain =
70
+ options.initialXDomains?.[track.id] ??
71
+ options.initialXDomains?.[seriesId] ??
72
+ options.initialXDomain
73
+ return explicitDomain
74
+ ? applyXDomainStrategy(explicitDomain, strategy, true)
75
+ : applyXDomainStrategy(track.xDomain, strategy)
76
+ }
77
+
60
78
  export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[] {
61
79
  const visibleCells = options.cells.map((cell) => ({ ...cell, hasSeries: Boolean(cell.series) }))
62
80
  const bottomCells = getBottomRowCellIndexes(visibleCells, options.grid.columnCount)
@@ -88,13 +106,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
88
106
  const series = displayTrack.visibleSeries[0] ?? displayTrack.series[0] ?? emptySeries
89
107
  const baseXScale =
90
108
  options.displayMode === 'independent'
91
- ? scaleLinear(
92
- options.initialXDomains?.[displayTrack.id] ??
93
- options.initialXDomains?.[series.id] ??
94
- options.initialXDomain ??
95
- displayTrack.xDomain,
96
- [0, cell.width],
97
- )
109
+ ? scaleLinear(resolveIndependentXDomain(displayTrack, series.id, options), [0, cell.width])
98
110
  : scaleLinear(options.sharedZoomDomain, [0, cell.width])
99
111
  const transform =
100
112
  options.displayMode === 'independent'
@@ -204,6 +204,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
204
204
  () => props.minVisiblePoints,
205
205
  () => props.initialXDomain,
206
206
  () => props.initialXDomains,
207
+ () => props.xDomainStrategy,
207
208
  () => props.displayMode,
208
209
  () => chartTracks.value.length,
209
210
  currentPage,
@@ -34,6 +34,7 @@ import {
34
34
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
35
35
  import type { PreparedWaveformSeries } from './useWaveformData'
36
36
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
37
+ import { applyXDomainStrategy } from './xDomain'
37
38
  import type { useWaveformAnnotationInteraction } from '../annotation'
38
39
 
39
40
  interface LayoutContext {
@@ -224,9 +225,13 @@ export function useWaveformLayout(context: LayoutContext) {
224
225
  Number.isFinite(domain[1]) &&
225
226
  domain[0] !== domain[1]
226
227
  ) {
227
- return domain[0] < domain[1] ? domain : [domain[1], domain[0]]
228
+ return applyXDomainStrategy(
229
+ domain[0] < domain[1] ? domain : [domain[1], domain[0]],
230
+ props.xDomainStrategy,
231
+ true,
232
+ )
228
233
  }
229
- return sharedXDomain.value
234
+ return applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy)
230
235
  })
231
236
  const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
232
237
  const configuredDomain =
@@ -239,11 +244,18 @@ export function useWaveformLayout(context: LayoutContext) {
239
244
  Number.isFinite(configuredDomain[1]) &&
240
245
  configuredDomain[0] !== configuredDomain[1]
241
246
  ) {
242
- return configuredDomain[0] < configuredDomain[1]
243
- ? configuredDomain
244
- : [configuredDomain[1], configuredDomain[0]]
247
+ return applyXDomainStrategy(
248
+ configuredDomain[0] < configuredDomain[1]
249
+ ? configuredDomain
250
+ : [configuredDomain[1], configuredDomain[0]],
251
+ props.xDomainStrategy,
252
+ true,
253
+ )
245
254
  }
246
- return paddedDomain(track.seriesList.flatMap((series) => series.xDomain))
255
+ return applyXDomainStrategy(
256
+ paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
257
+ props.xDomainStrategy,
258
+ )
247
259
  }
248
260
  const sharedZoomDomain = computed(
249
261
  () =>
@@ -272,6 +284,7 @@ export function useWaveformLayout(context: LayoutContext) {
272
284
  sharedZoomDomain: sharedZoomDomain.value,
273
285
  initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
274
286
  initialXDomains: props.initialXDomains,
287
+ xDomainStrategy: props.xDomainStrategy,
275
288
  fixedYDomain: props.yDomain,
276
289
  fixedYDomains: props.yDomains,
277
290
  yDomains:
@@ -11,6 +11,7 @@ import type {
11
11
  WaveformPoint,
12
12
  WaveformRenderingOptions,
13
13
  WaveformTitleOptions,
14
+ WaveformXDomainStrategy,
14
15
  WaveformZeroLineOptions,
15
16
  WaveformZoomEndPayload,
16
17
  WaveformZoomResetPayload,
@@ -33,6 +34,7 @@ export interface WaveformChartProps {
33
34
  minVisiblePoints?: number
34
35
  initialXDomain?: [number, number]
35
36
  initialXDomains?: Record<string, [number, number]>
37
+ xDomainStrategy?: WaveformXDomainStrategy
36
38
  yDomain?: [number, number]
37
39
  yDomains?: Record<string, [number, number]>
38
40
  timeUnit?: 's' | 'ms'
@@ -63,6 +65,7 @@ type DefaultedProp =
63
65
  | 'zoomable'
64
66
  | 'pannable'
65
67
  | 'minVisiblePoints'
68
+ | 'xDomainStrategy'
66
69
  | 'timeUnit'
67
70
  | 'annotations'
68
71
  | 'annotationsVisible'
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { applyXDomainStrategy } from './xDomain'
4
+
5
+ describe('applyXDomainStrategy', () => {
6
+ it('keeps the exact data domain by default', () => {
7
+ expect(applyXDomainStrategy([0, 4999.999], { type: 'data' })).toEqual([0, 4999.999])
8
+ })
9
+
10
+ it('expands both bounds to stable nice values', () => {
11
+ expect(applyXDomainStrategy([123, 456], { type: 'nice' })).toEqual([100, 500])
12
+ })
13
+
14
+ it('can expand only the end bound', () => {
15
+ expect(applyXDomainStrategy([123, 456], { type: 'nice', bounds: 'end' })).toEqual([123, 500])
16
+ })
17
+
18
+ it('keeps explicit domains exact unless they are included', () => {
19
+ expect(applyXDomainStrategy([123, 456], { type: 'nice' }, true)).toEqual([123, 456])
20
+ expect(applyXDomainStrategy([123, 456], { type: 'nice', includeExplicit: true }, true)).toEqual(
21
+ [100, 500],
22
+ )
23
+ })
24
+
25
+ it('falls back to the default tick count for invalid values', () => {
26
+ expect(applyXDomainStrategy([0, 4999.999], { type: 'nice', tickCount: 0 })).toEqual([0, 5000])
27
+ })
28
+ })
@@ -0,0 +1,25 @@
1
+ import { scaleLinear } from 'd3'
2
+
3
+ import type { WaveformXDomainStrategy } from '../../types'
4
+
5
+ const DEFAULT_NICE_TICK_COUNT = 10
6
+
7
+ function resolveTickCount(value: number | undefined): number {
8
+ if (!Number.isFinite(value) || (value ?? 0) < 1) return DEFAULT_NICE_TICK_COUNT
9
+ return Math.max(1, Math.trunc(value as number))
10
+ }
11
+
12
+ /** Derives a viewport domain without changing any source coordinates. */
13
+ export function applyXDomainStrategy(
14
+ domain: [number, number],
15
+ strategy: WaveformXDomainStrategy,
16
+ explicit = false,
17
+ ): [number, number] {
18
+ if (strategy.type !== 'nice' || (explicit && !strategy.includeExplicit)) return [...domain]
19
+
20
+ const niceDomain = scaleLinear()
21
+ .domain(domain)
22
+ .nice(resolveTickCount(strategy.tickCount))
23
+ .domain() as [number, number]
24
+ return strategy.bounds === 'end' ? [domain[0], niceDomain[1]] : niceDomain
25
+ }
@@ -9,6 +9,7 @@ export type {
9
9
  WaveformDisplayMode,
10
10
  WaveformOverlayMode,
11
11
  WaveformInteractionMode,
12
+ WaveformXDomainStrategy,
12
13
  WaveformZoomEndPayload,
13
14
  WaveformZoomResetPayload,
14
15
  WaveformAnnotationStyle,
@@ -9,6 +9,7 @@ export type {
9
9
  WaveformDisplayMode,
10
10
  WaveformOverlayMode,
11
11
  WaveformInteractionMode,
12
+ WaveformXDomainStrategy,
12
13
  WaveformAnnotationStyle,
13
14
  WaveformAnnotation,
14
15
  WaveformRenderingOptions,
@@ -5,6 +5,13 @@ import { flushAnimationFrames } from '../../test/setup'
5
5
  import { gridSeries, mountSizedChart } from '../../test/waveformChart'
6
6
 
7
7
  describe('WaveformChart viewport reset', () => {
8
+ const niceExplicitStrategy = {
9
+ type: 'nice' as const,
10
+ bounds: 'end' as const,
11
+ tickCount: 10,
12
+ includeExplicit: true,
13
+ }
14
+
8
15
  it('resets a shared viewport on double-click and emits a global payload', async () => {
9
16
  const wrapper = await mountSizedChart(
10
17
  {
@@ -111,4 +118,32 @@ describe('WaveformChart viewport reset', () => {
111
118
  expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
112
119
  expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
113
120
  })
121
+
122
+ it('resets shared and independent explicit domains to their included nice bounds', async () => {
123
+ const shared = await mountSizedChart(
124
+ {
125
+ kind: 'points',
126
+ points: [
127
+ { x: 0, y: 0 },
128
+ { x: 4.999999, y: 1 },
129
+ ],
130
+ },
131
+ { initialXDomain: [0, 4.999999], xDomainStrategy: niceExplicitStrategy },
132
+ )
133
+ const independent = await mountSizedChart(gridSeries(2), {
134
+ displayMode: 'independent',
135
+ initialXDomains: { 'channel-0': [0, 4.999999] },
136
+ xDomainStrategy: niceExplicitStrategy,
137
+ })
138
+
139
+ ;(shared.vm as unknown as { resetViewport: () => void }).resetViewport()
140
+ ;(independent.vm as unknown as { resetViewport: () => void }).resetViewport()
141
+ await flushPromises()
142
+
143
+ expect(shared.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
144
+ expect(shared.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
145
+ const firstTrack = independent.findAll('.waveform-chart__track')[0]
146
+ expect(firstTrack.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
147
+ expect(firstTrack.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
148
+ })
114
149
  })
@@ -0,0 +1,96 @@
1
+ import { describe, expect, it } from 'vitest'
2
+
3
+ import { gridSeries, mountSizedChart } from '../../test/waveformChart'
4
+
5
+ describe('WaveformChart x domain strategy', () => {
6
+ it('expands automatic x domains without changing source seconds', async () => {
7
+ const data = {
8
+ kind: 'points' as const,
9
+ points: [
10
+ { x: 0, y: 0 },
11
+ { x: 4.999999, y: 1 },
12
+ ],
13
+ }
14
+ const wrapper = await mountSizedChart(data, {
15
+ timeUnit: 'ms',
16
+ xDomainStrategy: { type: 'nice', bounds: 'end' },
17
+ })
18
+
19
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
20
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
21
+ expect(data.points[1]?.x).toBe(4.999999)
22
+ })
23
+
24
+ it('keeps explicit initial x domains exact by default', async () => {
25
+ const wrapper = await mountSizedChart(
26
+ {
27
+ kind: 'points',
28
+ points: [
29
+ { x: 123, y: 0 },
30
+ { x: 456, y: 1 },
31
+ ],
32
+ },
33
+ {
34
+ timeUnit: 's',
35
+ initialXDomain: [120, 460],
36
+ xDomainStrategy: { type: 'nice' },
37
+ },
38
+ )
39
+
40
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('120')
41
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('460')
42
+ })
43
+
44
+ it('keeps explicit per-track initial x domains exact by default', async () => {
45
+ const wrapper = await mountSizedChart(gridSeries(1), {
46
+ displayMode: 'independent',
47
+ initialXDomains: { 'channel-0': [0, 4.999999] },
48
+ xDomainStrategy: { type: 'nice', bounds: 'end' },
49
+ })
50
+
51
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
52
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4999.999')
53
+ })
54
+
55
+ it('includes an explicit shared initial x domain when requested', async () => {
56
+ const wrapper = await mountSizedChart(
57
+ {
58
+ kind: 'points',
59
+ points: [
60
+ { x: 0, y: 0 },
61
+ { x: 4.999999, y: 1 },
62
+ ],
63
+ },
64
+ {
65
+ initialXDomain: [0, 4.999999],
66
+ xDomainStrategy: {
67
+ type: 'nice',
68
+ bounds: 'end',
69
+ tickCount: 10,
70
+ includeExplicit: true,
71
+ },
72
+ },
73
+ )
74
+
75
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
76
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
77
+ })
78
+
79
+ it('includes explicit per-track initial x domains in independent mode', async () => {
80
+ const wrapper = await mountSizedChart(gridSeries(2), {
81
+ displayMode: 'independent',
82
+ grid: { rowCount: 1, columnCount: 2 },
83
+ initialXDomains: {
84
+ 'channel-0': [0, 4.999999],
85
+ 'channel-1': [10, 14.999999],
86
+ },
87
+ xDomainStrategy: { type: 'nice', bounds: 'end', tickCount: 10, includeExplicit: true },
88
+ })
89
+
90
+ const tracks = wrapper.findAll('.waveform-chart__track')
91
+ expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
92
+ expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
93
+ expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('10000')
94
+ expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('15000')
95
+ })
96
+ })
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  WaveformDisplayMode,
14
14
  WaveformOverlayMode,
15
15
  WaveformInteractionMode,
16
+ WaveformXDomainStrategy,
16
17
  WaveformZoomEndPayload,
17
18
  WaveformZoomResetPayload,
18
19
  WaveformAnnotationStyle,
@@ -26,6 +26,17 @@ export type WaveformOverlayMode = 'single-axis' | 'multi-axis'
26
26
  /** 标注工具模式 */
27
27
  export type WaveformInteractionMode = 'zoom' | 'annotation'
28
28
 
29
+ /** Controls how the initial X viewport is derived when no explicit domain is configured. */
30
+ export interface WaveformXDomainStrategy {
31
+ type: 'data' | 'nice'
32
+ /** Selects which bounds are expanded when type is `nice`. Defaults to `both`. */
33
+ bounds?: 'both' | 'end'
34
+ /** Stable tick count used to calculate nice bounds. Defaults to 10. */
35
+ tickCount?: number
36
+ /** Applies the strategy to explicit initial domains. Defaults to false. */
37
+ includeExplicit?: boolean
38
+ }
39
+
29
40
  /** Describes the X-axis viewport after a zoom gesture completes. */
30
41
  export interface WaveformZoomEndPayload {
31
42
  start: number
@@ -8,6 +8,7 @@ export type {
8
8
  WaveformDisplayMode,
9
9
  WaveformOverlayMode,
10
10
  WaveformInteractionMode,
11
+ WaveformXDomainStrategy,
11
12
  WaveformZoomEndPayload,
12
13
  WaveformZoomResetPayload,
13
14
  WaveformAnnotationStyle,