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.
- package/README.md +26 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +430 -401
- package/dist/types/components/WaveformChart.vue.d.ts +3 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +4 -3
- package/dist/types/components/core/xDomain.d.ts +3 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +15 -0
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/core/trackLayoutBuilder.ts +19 -7
- package/src/components/core/useWaveformChartLifecycle.ts +1 -0
- package/src/components/core/useWaveformLayout.ts +19 -6
- package/src/components/core/waveformChartTypes.ts +5 -1
- package/src/components/core/xDomain.test.ts +28 -0
- package/src/components/core/xDomain.ts +25 -0
- package/src/components/data/types.ts +2 -0
- package/src/components/interaction/useWaveformViewport.ts +15 -1
- package/src/components/waveform.ts +1 -0
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +0 -65
- package/src/components/waveformChartCases/viewportReset.test.ts +149 -0
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +96 -0
- package/src/index.ts +2 -0
- package/src/types/chart.ts +17 -0
- 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';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
@@ -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
|
|
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
|
|
243
|
-
|
|
244
|
-
|
|
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
|
|
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
|
+
}
|
|
@@ -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,
|
|
@@ -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
|
})
|