waveform-analysis 0.1.31 → 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.
Files changed (30) hide show
  1. package/README.md +26 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +430 -401
  4. package/dist/types/components/WaveformChart.vue.d.ts +3 -2
  5. package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
  6. package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
  7. package/dist/types/components/core/waveformChartTypes.d.ts +4 -3
  8. package/dist/types/components/core/xDomain.d.ts +3 -0
  9. package/dist/types/components/data/types.d.ts +1 -1
  10. package/dist/types/components/waveform.d.ts +1 -1
  11. package/dist/types/index.d.ts +1 -1
  12. package/dist/types/types/chart.d.ts +15 -0
  13. package/dist/types/types/index.d.ts +1 -1
  14. package/package.json +1 -1
  15. package/src/components/WaveformChart.vue +1 -0
  16. package/src/components/core/trackLayoutBuilder.ts +19 -7
  17. package/src/components/core/useWaveformChartLifecycle.ts +1 -0
  18. package/src/components/core/useWaveformLayout.ts +19 -6
  19. package/src/components/core/waveformChartTypes.ts +5 -1
  20. package/src/components/core/xDomain.test.ts +28 -0
  21. package/src/components/core/xDomain.ts +25 -0
  22. package/src/components/data/types.ts +2 -0
  23. package/src/components/interaction/useWaveformViewport.ts +15 -1
  24. package/src/components/waveform.ts +1 -0
  25. package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -65
  26. package/src/components/waveformChartCases/viewportReset.test.ts +149 -0
  27. package/src/components/waveformChartCases/xDomainStrategy.test.ts +96 -0
  28. package/src/index.ts +2 -0
  29. package/src/types/chart.ts +17 -0
  30. package/src/types/index.ts +2 -0
@@ -5,7 +5,7 @@ declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
5
5
  "point-hover": (point: import("..").WaveformPoint | null) => any;
6
6
  "zoom-change": (domain: [number, number]) => any;
7
7
  "zoom-end": (payload: import("..").WaveformZoomEndPayload) => any;
8
- "zoom-reset": () => any;
8
+ "zoom-reset": (payload: import("..").WaveformZoomResetPayload) => any;
9
9
  "update:annotations": (annotations: import("..").WaveformAnnotation[]) => any;
10
10
  "update:hidden-series-ids": (ids: string[]) => any;
11
11
  "series-visibility-change": (payload: {
@@ -21,7 +21,7 @@ declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
21
21
  "onPoint-hover"?: ((point: import("..").WaveformPoint | null) => any) | undefined;
22
22
  "onZoom-change"?: ((domain: [number, number]) => any) | undefined;
23
23
  "onZoom-end"?: ((payload: import("..").WaveformZoomEndPayload) => any) | undefined;
24
- "onZoom-reset"?: (() => any) | undefined;
24
+ "onZoom-reset"?: ((payload: import("..").WaveformZoomResetPayload) => any) | undefined;
25
25
  "onUpdate:annotations"?: ((annotations: import("..").WaveformAnnotation[]) => any) | undefined;
26
26
  "onUpdate:hidden-series-ids"?: ((ids: string[]) => any) | undefined;
27
27
  "onSeries-visibility-change"?: ((payload: {
@@ -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 } 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,13 +37,13 @@ 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;
43
44
  (event: 'zoom-change', domain: [number, number]): void;
44
45
  (event: 'zoom-end', payload: WaveformZoomEndPayload): void;
45
- (event: 'zoom-reset'): void;
46
+ (event: 'zoom-reset', payload: WaveformZoomResetPayload): void;
46
47
  (event: 'update:annotations', annotations: WaveformAnnotation[]): void;
47
48
  (event: 'update:hidden-series-ids', ids: string[]): void;
48
49
  (event: 'series-visibility-change', payload: {
@@ -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, 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, 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;
@@ -33,6 +43,11 @@ export interface WaveformZoomEndPayload {
33
43
  seriesIds?: string[];
34
44
  gesture?: 'wheel' | 'box';
35
45
  }
46
+ /** Identifies the viewport reset by a double-click gesture. */
47
+ export interface WaveformZoomResetPayload {
48
+ trackIndex?: number;
49
+ seriesIds?: string[];
50
+ }
36
51
  /** 标注颜色样式 */
37
52
  export interface WaveformAnnotationStyle {
38
53
  borderColor?: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, 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.31",
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,8 +11,10 @@ import type {
11
11
  WaveformPoint,
12
12
  WaveformRenderingOptions,
13
13
  WaveformTitleOptions,
14
+ WaveformXDomainStrategy,
14
15
  WaveformZeroLineOptions,
15
16
  WaveformZoomEndPayload,
17
+ WaveformZoomResetPayload,
16
18
  } from '../data/types'
17
19
  import type { WaveformGridOptions } from './grid'
18
20
 
@@ -32,6 +34,7 @@ export interface WaveformChartProps {
32
34
  minVisiblePoints?: number
33
35
  initialXDomain?: [number, number]
34
36
  initialXDomains?: Record<string, [number, number]>
37
+ xDomainStrategy?: WaveformXDomainStrategy
35
38
  yDomain?: [number, number]
36
39
  yDomains?: Record<string, [number, number]>
37
40
  timeUnit?: 's' | 'ms'
@@ -62,6 +65,7 @@ type DefaultedProp =
62
65
  | 'zoomable'
63
66
  | 'pannable'
64
67
  | 'minVisiblePoints'
68
+ | 'xDomainStrategy'
65
69
  | 'timeUnit'
66
70
  | 'annotations'
67
71
  | 'annotationsVisible'
@@ -82,7 +86,7 @@ export interface WaveformChartEmit {
82
86
  (event: 'point-hover', point: WaveformPoint | null): void
83
87
  (event: 'zoom-change', domain: [number, number]): void
84
88
  (event: 'zoom-end', payload: WaveformZoomEndPayload): void
85
- (event: 'zoom-reset'): void
89
+ (event: 'zoom-reset', payload: WaveformZoomResetPayload): void
86
90
  (event: 'update:annotations', annotations: WaveformAnnotation[]): void
87
91
  (event: 'update:hidden-series-ids', ids: string[]): void
88
92
  (
@@ -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,7 +9,9 @@ export type {
9
9
  WaveformDisplayMode,
10
10
  WaveformOverlayMode,
11
11
  WaveformInteractionMode,
12
+ WaveformXDomainStrategy,
12
13
  WaveformZoomEndPayload,
14
+ WaveformZoomResetPayload,
13
15
  WaveformAnnotationStyle,
14
16
  WaveformAnnotation,
15
17
  WaveformRenderingOptions,
@@ -346,9 +346,23 @@ export function useWaveformViewport(context: ViewportContext) {
346
346
  }
347
347
  const requestViewportReset = (event: MouseEvent) => {
348
348
  if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
349
+ if (props.displayMode === 'independent') {
350
+ const target = event.target instanceof Element ? event.target : null
351
+ const overlay = target?.closest('[data-independent-overlay-index]')
352
+ const trackIndex = Number(overlay?.getAttribute('data-independent-overlay-index'))
353
+ const track = trackLayouts.value.find((item) => item.index === trackIndex)
354
+ if (!track) return
355
+ event.preventDefault()
356
+ resetViewport(trackIndex)
357
+ emit('zoom-reset', {
358
+ trackIndex,
359
+ seriesIds: track.legendSeries.map((series) => series.id),
360
+ })
361
+ return
362
+ }
349
363
  event.preventDefault()
350
364
  resetViewport()
351
- emit('zoom-reset')
365
+ emit('zoom-reset', {})
352
366
  }
353
367
  return {
354
368
  selectionBox,
@@ -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,
@@ -308,69 +308,4 @@ describe('WaveformChart', () => {
308
308
  expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
309
309
  expect(zoomedOutDomain).toEqual([0, 2])
310
310
  })
311
-
312
- it('resets a shared viewport on double-click and emits zoom-reset', async () => {
313
- const wrapper = await mountSizedChart({
314
- kind: 'points',
315
- points: [
316
- { x: 0, y: 0 },
317
- { x: 2, y: 1 },
318
- ],
319
- })
320
- const overlay = wrapper.get('.waveform-chart__overlay')
321
- const width = Number(overlay.attributes('width'))
322
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
323
- value: () => ({ left: 0, top: 0, width, height: 290 }),
324
- })
325
- overlay.element.dispatchEvent(
326
- new WheelEvent('wheel', {
327
- deltaY: -4000,
328
- clientX: width / 2,
329
- clientY: 145,
330
- bubbles: true,
331
- cancelable: true,
332
- }),
333
- )
334
- flushAnimationFrames()
335
- await flushPromises()
336
- expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
337
-
338
- overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
339
- await flushPromises()
340
-
341
- expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
342
- expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
343
- expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
344
- })
345
-
346
- it('exposes resetViewport for independent tracks', async () => {
347
- const wrapper = await mountSizedChart(gridSeries(2), {
348
- displayMode: 'independent',
349
- grid: { rowCount: 1, columnCount: 2 },
350
- })
351
- const overlay = wrapper.findAll('.waveform-chart__overlay--independent')[0]
352
- const width = Number(overlay.attributes('width'))
353
- Object.defineProperty(overlay.element, 'getBoundingClientRect', {
354
- value: () => ({ left: 0, top: 0, width, height: 260 }),
355
- })
356
- overlay.element.dispatchEvent(
357
- new WheelEvent('wheel', {
358
- deltaY: -4000,
359
- clientX: width / 2,
360
- clientY: 130,
361
- bubbles: true,
362
- cancelable: true,
363
- }),
364
- )
365
- flushAnimationFrames()
366
- await flushPromises()
367
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
368
-
369
- const chart = wrapper.vm as unknown as { resetViewport: () => void }
370
- chart.resetViewport()
371
- await flushPromises()
372
-
373
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
374
- expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
375
- })
376
311
  })