waveform-analysis 0.1.28 → 0.1.29
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 +57 -33
- package/dist/index.cjs +1 -1
- package/dist/index.js +552 -548
- package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
- package/dist/types/demo/types.d.ts +9 -0
- package/dist/types/demo/useDemoXAxisLabelControls.d.ts +32 -0
- 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/dist/types/utils/formatters.d.ts +5 -2
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/App.vue +7 -1
- package/src/components/core/trackLayoutBuilder.ts +22 -4
- package/src/components/core/useWaveformLayout.ts +1 -0
- package/src/components/rendering/WaveformTrackAxes.vue +6 -3
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +70 -3
- package/src/demo/DemoGridFrameControls.vue +70 -0
- package/src/demo/types.ts +9 -0
- package/src/demo/useDemoXAxisLabelControls.ts +77 -0
- package/src/demo/xAxisLabelControls.test.ts +86 -0
- package/src/index.ts +3 -0
- package/src/styles.css +8 -0
- package/src/types/chart.ts +21 -0
- package/src/types/index.ts +3 -0
- package/src/utils/formatters.test.ts +37 -5
- package/src/utils/formatters.ts +26 -16
- package/src/utils/index.ts +1 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ZoomTransform } from 'd3';
|
|
2
2
|
import { type ResolvedWaveformRenderingOptions } from '../../core/rendering';
|
|
3
|
-
import type { WaveformDisplayMode, WaveformOverlayMode } from '../../types';
|
|
3
|
+
import type { WaveformDisplayMode, WaveformOverlayMode, 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 {
|
|
@@ -19,6 +19,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
19
19
|
fixedYDomains?: Record<string, [number, number]>;
|
|
20
20
|
yDomains?: Record<string, [number, number]>;
|
|
21
21
|
timeUnit: 's' | 'ms';
|
|
22
|
+
xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
|
|
22
23
|
rendering: ResolvedWaveformRenderingOptions;
|
|
23
24
|
hideSecondaryLabels: boolean;
|
|
24
25
|
yAxisLabelX: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
|
|
2
|
+
import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls';
|
|
2
3
|
interface SelectOption<T> {
|
|
3
4
|
label: string;
|
|
4
5
|
value: T;
|
|
@@ -27,6 +28,14 @@ export interface DemoControlPanelModel {
|
|
|
27
28
|
verticalGridColor: string;
|
|
28
29
|
xAxisLineVisible: boolean;
|
|
29
30
|
yAxisLineVisible: boolean;
|
|
31
|
+
xAxisLabelFormatterEnabled: boolean;
|
|
32
|
+
xAxisLabelFormat: DemoXAxisLabelFormat;
|
|
33
|
+
xAxisLabelFormatOptions: Array<SelectOption<DemoXAxisLabelFormat>>;
|
|
34
|
+
xAxisLabelMultiplier: number;
|
|
35
|
+
xAxisLabelFractionDigits: number;
|
|
36
|
+
xAxisLabelTimeZone: DemoXAxisLabelTimeZone;
|
|
37
|
+
xAxisLabelTimeZoneOptions: Array<SelectOption<DemoXAxisLabelTimeZone>>;
|
|
38
|
+
xAxisLabelShowMilliseconds: boolean;
|
|
30
39
|
frameBorderColor: string;
|
|
31
40
|
frameBackgroundColor: string;
|
|
32
41
|
frameBorderWidth: number;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { WaveformXAxisLabelFormatter } from '../types';
|
|
2
|
+
export type DemoXAxisLabelFormat = 'number' | 'datetime';
|
|
3
|
+
export type DemoXAxisLabelTimeZone = 'local' | 'Asia/Shanghai' | 'UTC';
|
|
4
|
+
export declare function formatDemoTimestamp(value: number, timeZone: DemoXAxisLabelTimeZone, showMilliseconds: boolean): string;
|
|
5
|
+
export declare function useDemoXAxisLabelControls(): {
|
|
6
|
+
controlModel: {
|
|
7
|
+
xAxisLabelFormatterEnabled: import("vue").Ref<boolean, boolean>;
|
|
8
|
+
xAxisLabelFormat: import("vue").Ref<DemoXAxisLabelFormat, DemoXAxisLabelFormat>;
|
|
9
|
+
xAxisLabelFormatOptions: ({
|
|
10
|
+
label: string;
|
|
11
|
+
value: "number";
|
|
12
|
+
} | {
|
|
13
|
+
label: string;
|
|
14
|
+
value: "datetime";
|
|
15
|
+
})[];
|
|
16
|
+
xAxisLabelMultiplier: import("vue").Ref<number, number>;
|
|
17
|
+
xAxisLabelFractionDigits: import("vue").Ref<number, number>;
|
|
18
|
+
xAxisLabelTimeZone: import("vue").Ref<DemoXAxisLabelTimeZone, DemoXAxisLabelTimeZone>;
|
|
19
|
+
xAxisLabelTimeZoneOptions: ({
|
|
20
|
+
label: string;
|
|
21
|
+
value: "Asia/Shanghai";
|
|
22
|
+
} | {
|
|
23
|
+
label: string;
|
|
24
|
+
value: "local";
|
|
25
|
+
} | {
|
|
26
|
+
label: string;
|
|
27
|
+
value: "UTC";
|
|
28
|
+
})[];
|
|
29
|
+
xAxisLabelShowMilliseconds: import("vue").Ref<boolean, boolean>;
|
|
30
|
+
};
|
|
31
|
+
xAxisLabelFormatter: import("vue").ComputedRef<WaveformXAxisLabelFormatter | undefined>;
|
|
32
|
+
};
|
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, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
|
|
6
|
+
export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, 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';
|
|
@@ -106,10 +106,25 @@ export interface WaveformFrameStyle {
|
|
|
106
106
|
borderStyle?: 'solid' | 'dashed' | 'dotted';
|
|
107
107
|
backgroundColor?: string;
|
|
108
108
|
}
|
|
109
|
+
export type WaveformXAxisLabelKind = 'tick' | 'start' | 'end';
|
|
110
|
+
/** Context passed to custom X-axis label formatters. */
|
|
111
|
+
export interface WaveformXAxisLabelFormatterContext {
|
|
112
|
+
kind: WaveformXAxisLabelKind;
|
|
113
|
+
/** X coordinate in the source data, before time-unit conversion. */
|
|
114
|
+
rawValue: number;
|
|
115
|
+
timeUnit: 's' | 'ms';
|
|
116
|
+
/** Current visible X domain in source coordinates. */
|
|
117
|
+
domain: [number, number];
|
|
118
|
+
/** Current visible X domain converted to the selected display unit. */
|
|
119
|
+
displayDomain: [number, number];
|
|
120
|
+
}
|
|
121
|
+
export type WaveformXAxisLabelFormatter = (value: number, context: WaveformXAxisLabelFormatterContext) => string;
|
|
109
122
|
/** Controls axis baseline visibility while preserving tick marks and axis text. */
|
|
110
123
|
export interface WaveformAxesOptions {
|
|
111
124
|
x?: {
|
|
112
125
|
lineVisible?: boolean;
|
|
126
|
+
/** Formats display-unit X values for ticks and visible-range endpoints. */
|
|
127
|
+
labelFormatter?: WaveformXAxisLabelFormatter;
|
|
113
128
|
};
|
|
114
129
|
y?: {
|
|
115
130
|
lineVisible?: boolean;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* 类型定义统一导出
|
|
3
3
|
*/
|
|
4
|
-
export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
|
|
4
|
+
export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, 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';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types';
|
|
1
2
|
/**
|
|
2
3
|
* 时间单位类型
|
|
3
4
|
*/
|
|
@@ -33,6 +34,8 @@ export declare function formatPlainNumber(value: number): string;
|
|
|
33
34
|
* @returns 转换后的显示值
|
|
34
35
|
*/
|
|
35
36
|
export declare function displayTime(value: number, timeUnit: TimeUnit): number;
|
|
37
|
+
/** Format an X-axis value without changing its source coordinate. */
|
|
38
|
+
export declare function formatXAxisLabel(rawValue: number, domain: [number, number], timeUnit: TimeUnit, kind: WaveformXAxisLabelKind, formatter?: WaveformXAxisLabelFormatter): string;
|
|
36
39
|
/**
|
|
37
40
|
* 计算端点标签的小数位数
|
|
38
41
|
* @param domain 数据域 [最小值, 最大值]
|
|
@@ -41,7 +44,7 @@ export declare function displayTime(value: number, timeUnit: TimeUnit): number;
|
|
|
41
44
|
*/
|
|
42
45
|
export declare function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number;
|
|
43
46
|
/**
|
|
44
|
-
* 将 X
|
|
47
|
+
* 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
|
|
45
48
|
* @param value 时间值(秒)
|
|
46
49
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
47
50
|
* @param timeUnit 时间单位
|
|
@@ -49,7 +52,7 @@ export declare function endpointFractionDigits(domain: [number, number], timeUni
|
|
|
49
52
|
*/
|
|
50
53
|
export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
|
|
51
54
|
/**
|
|
52
|
-
* 将 X
|
|
55
|
+
* 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
|
|
53
56
|
* @param value 时间值(秒)
|
|
54
57
|
* @param timeUnit 时间单位
|
|
55
58
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* 工具函数模块统一导出
|
|
3
3
|
*/
|
|
4
4
|
export { paddedDomain, buildMinorTicks } from './domain';
|
|
5
|
-
export { displayTime, endpointFractionDigits, formatEndpointTime, formatAxisTime, formatTooltipTime, formatAnnotationTime, formatPlainNumber, formatScientificAxisLabel, formatScientificAxisExponent, formatScientificYAxisLabel, formatTooltipNumber, resolveScientificAxisExponent, shouldUseScientificAxisLabel, shouldUseScientificYAxisLabel, type ScientificAxisLabelOptions, type ScientificYAxisLabelOptions, type TimeUnit, } from './formatters';
|
|
5
|
+
export { displayTime, endpointFractionDigits, formatEndpointTime, formatAxisTime, formatXAxisLabel, formatTooltipTime, formatAnnotationTime, formatPlainNumber, formatScientificAxisLabel, formatScientificAxisExponent, formatScientificYAxisLabel, formatTooltipNumber, resolveScientificAxisExponent, shouldUseScientificAxisLabel, shouldUseScientificYAxisLabel, type ScientificAxisLabelOptions, type ScientificYAxisLabelOptions, type TimeUnit, } from './formatters';
|
|
6
6
|
export { resolveTrackGeometry, clamp, type TrackGeometry } from './geometry';
|
|
7
7
|
export { downsampleLTTB, downsampleMinMax, adaptiveSampling, calculateSamplingThreshold, } from './sampling';
|
package/package.json
CHANGED
package/src/App.vue
CHANGED
|
@@ -23,6 +23,7 @@ import { createSimulatedWaveformData } from './data/simulatedWaveforms'
|
|
|
23
23
|
import DemoChartHost from './demo/DemoChartHost.vue'
|
|
24
24
|
import DemoControlPanel from './demo/DemoControlPanel.vue'
|
|
25
25
|
import type { DemoChartModel, DemoControlPanelModel } from './demo/types'
|
|
26
|
+
import { useDemoXAxisLabelControls } from './demo/useDemoXAxisLabelControls'
|
|
26
27
|
|
|
27
28
|
const fullChartData = createSimulatedWaveformData()
|
|
28
29
|
const displayMode = ref<WaveformDisplayMode>('independent')
|
|
@@ -40,6 +41,7 @@ const verticalGridVisible = ref(true)
|
|
|
40
41
|
const verticalGridColor = ref('#dfe5ef')
|
|
41
42
|
const xAxisLineVisible = ref(false)
|
|
42
43
|
const yAxisLineVisible = ref(false)
|
|
44
|
+
const { controlModel: xAxisLabelControlModel, xAxisLabelFormatter } = useDemoXAxisLabelControls()
|
|
43
45
|
const annotations = ref<WaveformAnnotation[]>([])
|
|
44
46
|
const annotationsVisible = ref(true)
|
|
45
47
|
const cleanView = ref(false)
|
|
@@ -112,7 +114,10 @@ const frameStyle = computed<WaveformFrameStyle>(() => ({
|
|
|
112
114
|
backgroundColor: frameBackgroundColor.value,
|
|
113
115
|
}))
|
|
114
116
|
const axes = computed<WaveformAxesOptions>(() => ({
|
|
115
|
-
x: {
|
|
117
|
+
x: {
|
|
118
|
+
lineVisible: xAxisLineVisible.value,
|
|
119
|
+
...(xAxisLabelFormatter.value ? { labelFormatter: xAxisLabelFormatter.value } : {}),
|
|
120
|
+
},
|
|
116
121
|
y: { lineVisible: yAxisLineVisible.value },
|
|
117
122
|
}))
|
|
118
123
|
const zeroLine = computed<WaveformZeroLineOptions>(() => ({
|
|
@@ -308,6 +313,7 @@ const controlPanelModel = reactive({
|
|
|
308
313
|
verticalGridColor,
|
|
309
314
|
xAxisLineVisible,
|
|
310
315
|
yAxisLineVisible,
|
|
316
|
+
...xAxisLabelControlModel,
|
|
311
317
|
frameBorderColor,
|
|
312
318
|
frameBackgroundColor,
|
|
313
319
|
frameBorderWidth,
|
|
@@ -12,8 +12,13 @@ import {
|
|
|
12
12
|
selectSeriesRenderPoints,
|
|
13
13
|
type ResolvedWaveformRenderingOptions,
|
|
14
14
|
} from '../../core/rendering'
|
|
15
|
-
import type {
|
|
16
|
-
|
|
15
|
+
import type {
|
|
16
|
+
WaveformDisplayMode,
|
|
17
|
+
WaveformOverlayMode,
|
|
18
|
+
WaveformPoint,
|
|
19
|
+
WaveformXAxisLabelFormatter,
|
|
20
|
+
} from '../../types'
|
|
21
|
+
import { buildMinorTicks, formatXAxisLabel } from '../../utils'
|
|
17
22
|
import {
|
|
18
23
|
getBottomRowCellIndexes,
|
|
19
24
|
type GridCellGeometry,
|
|
@@ -45,6 +50,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
45
50
|
fixedYDomains?: Record<string, [number, number]>
|
|
46
51
|
yDomains?: Record<string, [number, number]>
|
|
47
52
|
timeUnit: 's' | 'ms'
|
|
53
|
+
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
48
54
|
rendering: ResolvedWaveformRenderingOptions
|
|
49
55
|
hideSecondaryLabels: boolean
|
|
50
56
|
yAxisLabelX: number
|
|
@@ -147,8 +153,20 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
147
153
|
const yAxisTickValues = yAxes[0]?.tickValues ?? []
|
|
148
154
|
const domain = xScale.domain() as [number, number]
|
|
149
155
|
const endpointLabels = {
|
|
150
|
-
start:
|
|
151
|
-
|
|
156
|
+
start: formatXAxisLabel(
|
|
157
|
+
domain[0],
|
|
158
|
+
domain,
|
|
159
|
+
options.timeUnit,
|
|
160
|
+
'start',
|
|
161
|
+
options.xAxisLabelFormatter,
|
|
162
|
+
),
|
|
163
|
+
end: formatXAxisLabel(
|
|
164
|
+
domain[1],
|
|
165
|
+
domain,
|
|
166
|
+
options.timeUnit,
|
|
167
|
+
'end',
|
|
168
|
+
options.xAxisLabelFormatter,
|
|
169
|
+
),
|
|
152
170
|
}
|
|
153
171
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
|
154
172
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
|
@@ -285,6 +285,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
285
285
|
)
|
|
286
286
|
: sharedYDomains.value,
|
|
287
287
|
timeUnit: props.timeUnit,
|
|
288
|
+
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
288
289
|
rendering: renderingOptions.value,
|
|
289
290
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
290
291
|
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
|
@@ -3,7 +3,7 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
|
|
3
3
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
|
-
import {
|
|
6
|
+
import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
|
|
7
7
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
@@ -66,10 +66,12 @@ function renderAxes() {
|
|
|
66
66
|
axisBottom(props.track.xScale)
|
|
67
67
|
.tickValues(props.track.xAxisTickValues)
|
|
68
68
|
.tickFormat((value) =>
|
|
69
|
-
|
|
69
|
+
formatXAxisLabel(
|
|
70
70
|
Number(value),
|
|
71
|
-
props.timeUnit,
|
|
72
71
|
props.track.xScale.domain() as [number, number],
|
|
72
|
+
props.timeUnit,
|
|
73
|
+
'tick',
|
|
74
|
+
props.axes?.x?.labelFormatter,
|
|
73
75
|
),
|
|
74
76
|
)
|
|
75
77
|
.tickSize(-4)
|
|
@@ -93,6 +95,7 @@ watch(
|
|
|
93
95
|
() => props.track.xAxisTickValues,
|
|
94
96
|
() => props.track.yAxisTickValues,
|
|
95
97
|
() => props.timeUnit,
|
|
98
|
+
() => props.axes?.x?.labelFormatter,
|
|
96
99
|
() => props.axes?.x?.lineVisible,
|
|
97
100
|
() => props.axes?.y?.lineVisible,
|
|
98
101
|
],
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { flushPromises } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import type { WaveformXAxisLabelFormatter } from '../../types'
|
|
5
6
|
import { type WaveformData } from '../waveform'
|
|
6
7
|
|
|
7
8
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
@@ -162,7 +163,7 @@ describe('WaveformChart', () => {
|
|
|
162
163
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
|
163
164
|
String(trackWidth),
|
|
164
165
|
)
|
|
165
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
|
|
166
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990.3')
|
|
166
167
|
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
167
168
|
})
|
|
168
169
|
|
|
@@ -267,6 +268,70 @@ describe('WaveformChart', () => {
|
|
|
267
268
|
expect(endpointGroup.attributes('font-size')).toBe(xAxis.attributes('font-size'))
|
|
268
269
|
})
|
|
269
270
|
|
|
271
|
+
it('formats X-axis ticks and endpoints with display-unit values and source context', async () => {
|
|
272
|
+
const data: WaveformData = {
|
|
273
|
+
kind: 'points',
|
|
274
|
+
points: [
|
|
275
|
+
{ x: 0.125, y: 0 },
|
|
276
|
+
{ x: 1.875, y: 1 },
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
const originalPoints = data.points.map((point) => ({ ...point }))
|
|
280
|
+
const labelFormatter: WaveformXAxisLabelFormatter = (value, context) =>
|
|
281
|
+
`${context.kind}:${value / 10}`
|
|
282
|
+
const formatter = vi.fn(labelFormatter)
|
|
283
|
+
const wrapper = await mountSizedChart(data, {
|
|
284
|
+
timeUnit: 'ms',
|
|
285
|
+
axes: { x: { labelFormatter: formatter } },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('start:12.5')
|
|
289
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('end:187.5')
|
|
290
|
+
expect(
|
|
291
|
+
wrapper
|
|
292
|
+
.get('.waveform-chart__axis--x')
|
|
293
|
+
.findAll('.tick text')
|
|
294
|
+
.every((tick) => tick.text().startsWith('tick:')),
|
|
295
|
+
).toBe(true)
|
|
296
|
+
|
|
297
|
+
const startCall = formatter.mock.calls.find(([, context]) => context.kind === 'start')
|
|
298
|
+
const tickCall = formatter.mock.calls.find(([, context]) => context.kind === 'tick')
|
|
299
|
+
const endCall = formatter.mock.calls.find(([, context]) => context.kind === 'end')
|
|
300
|
+
expect(startCall).toEqual([
|
|
301
|
+
125,
|
|
302
|
+
{
|
|
303
|
+
kind: 'start',
|
|
304
|
+
rawValue: 0.125,
|
|
305
|
+
timeUnit: 'ms',
|
|
306
|
+
domain: [0.125, 1.875],
|
|
307
|
+
displayDomain: [125, 1875],
|
|
308
|
+
},
|
|
309
|
+
])
|
|
310
|
+
expect(tickCall?.[0]).toBeTypeOf('number')
|
|
311
|
+
expect(tickCall?.[1]).toMatchObject({
|
|
312
|
+
kind: 'tick',
|
|
313
|
+
timeUnit: 'ms',
|
|
314
|
+
domain: [0.125, 1.875],
|
|
315
|
+
displayDomain: [125, 1875],
|
|
316
|
+
})
|
|
317
|
+
expect(endCall?.[1]).toMatchObject({ kind: 'end', rawValue: 1.875 })
|
|
318
|
+
expect(data.points).toEqual(originalPoints)
|
|
319
|
+
|
|
320
|
+
await wrapper.setProps({
|
|
321
|
+
axes: { x: { labelFormatter: (value: number) => `updated:${value}` } },
|
|
322
|
+
})
|
|
323
|
+
await flushPromises()
|
|
324
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('updated:125')
|
|
325
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('updated:1875')
|
|
326
|
+
expect(
|
|
327
|
+
wrapper
|
|
328
|
+
.get('.waveform-chart__axis--x')
|
|
329
|
+
.findAll('.tick text')
|
|
330
|
+
.every((tick) => tick.text().startsWith('updated:')),
|
|
331
|
+
).toBe(true)
|
|
332
|
+
expect(data.points).toEqual(originalPoints)
|
|
333
|
+
})
|
|
334
|
+
|
|
270
335
|
it('keeps zoom-change domains in source seconds', async () => {
|
|
271
336
|
const wrapper = await mountSizedChart({
|
|
272
337
|
kind: 'points',
|
|
@@ -307,6 +372,8 @@ describe('WaveformChart', () => {
|
|
|
307
372
|
)
|
|
308
373
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
|
309
374
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
|
310
|
-
expect(
|
|
375
|
+
expect(
|
|
376
|
+
Number.isFinite(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())),
|
|
377
|
+
).toBe(true)
|
|
311
378
|
})
|
|
312
379
|
})
|
|
@@ -96,6 +96,76 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
|
96
96
|
<Switch v-model:checked="model.yAxisLineVisible" size="small" aria-label="显示纵轴线" />
|
|
97
97
|
</div>
|
|
98
98
|
</div>
|
|
99
|
+
<div class="x-axis-label-controls">
|
|
100
|
+
<label class="frame-style-control frame-style-control--switch">
|
|
101
|
+
<span>自定义 Label</span>
|
|
102
|
+
<Switch
|
|
103
|
+
v-model:checked="model.xAxisLabelFormatterEnabled"
|
|
104
|
+
size="small"
|
|
105
|
+
aria-label="自定义 X 轴 Label"
|
|
106
|
+
/>
|
|
107
|
+
</label>
|
|
108
|
+
<label v-if="model.xAxisLabelFormatterEnabled" class="frame-style-control">
|
|
109
|
+
<span>格式类型</span>
|
|
110
|
+
<Select
|
|
111
|
+
v-model:value="model.xAxisLabelFormat"
|
|
112
|
+
:options="model.xAxisLabelFormatOptions"
|
|
113
|
+
size="small"
|
|
114
|
+
aria-label="X 轴 Label 格式类型"
|
|
115
|
+
/>
|
|
116
|
+
</label>
|
|
117
|
+
<label
|
|
118
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'number'"
|
|
119
|
+
class="frame-style-control"
|
|
120
|
+
>
|
|
121
|
+
<span>运算倍率</span>
|
|
122
|
+
<InputNumber
|
|
123
|
+
v-model:value="model.xAxisLabelMultiplier"
|
|
124
|
+
:min="-1000000"
|
|
125
|
+
:max="1000000"
|
|
126
|
+
:step="0.1"
|
|
127
|
+
size="small"
|
|
128
|
+
aria-label="X 轴 Label 运算倍率"
|
|
129
|
+
/>
|
|
130
|
+
</label>
|
|
131
|
+
<label
|
|
132
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'number'"
|
|
133
|
+
class="frame-style-control"
|
|
134
|
+
>
|
|
135
|
+
<span>小数位数</span>
|
|
136
|
+
<InputNumber
|
|
137
|
+
v-model:value="model.xAxisLabelFractionDigits"
|
|
138
|
+
:min="0"
|
|
139
|
+
:max="12"
|
|
140
|
+
:step="1"
|
|
141
|
+
size="small"
|
|
142
|
+
aria-label="X 轴 Label 小数位数"
|
|
143
|
+
/>
|
|
144
|
+
</label>
|
|
145
|
+
<label
|
|
146
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'datetime'"
|
|
147
|
+
class="frame-style-control"
|
|
148
|
+
>
|
|
149
|
+
<span>时区</span>
|
|
150
|
+
<Select
|
|
151
|
+
v-model:value="model.xAxisLabelTimeZone"
|
|
152
|
+
:options="model.xAxisLabelTimeZoneOptions"
|
|
153
|
+
size="small"
|
|
154
|
+
aria-label="X 轴 Label 时区"
|
|
155
|
+
/>
|
|
156
|
+
</label>
|
|
157
|
+
<label
|
|
158
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'datetime'"
|
|
159
|
+
class="frame-style-control frame-style-control--switch"
|
|
160
|
+
>
|
|
161
|
+
<span>显示毫秒</span>
|
|
162
|
+
<Switch
|
|
163
|
+
v-model:checked="model.xAxisLabelShowMilliseconds"
|
|
164
|
+
size="small"
|
|
165
|
+
aria-label="X 轴 Label 显示毫秒"
|
|
166
|
+
/>
|
|
167
|
+
</label>
|
|
168
|
+
</div>
|
|
99
169
|
</section>
|
|
100
170
|
<section class="control-section">
|
|
101
171
|
<h2>图框样式</h2>
|
package/src/demo/types.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
WaveformZeroLineOptions,
|
|
15
15
|
WaveformZoomEndPayload,
|
|
16
16
|
} from '../components'
|
|
17
|
+
import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls'
|
|
17
18
|
|
|
18
19
|
interface SelectOption<T> {
|
|
19
20
|
label: string
|
|
@@ -44,6 +45,14 @@ export interface DemoControlPanelModel {
|
|
|
44
45
|
verticalGridColor: string
|
|
45
46
|
xAxisLineVisible: boolean
|
|
46
47
|
yAxisLineVisible: boolean
|
|
48
|
+
xAxisLabelFormatterEnabled: boolean
|
|
49
|
+
xAxisLabelFormat: DemoXAxisLabelFormat
|
|
50
|
+
xAxisLabelFormatOptions: Array<SelectOption<DemoXAxisLabelFormat>>
|
|
51
|
+
xAxisLabelMultiplier: number
|
|
52
|
+
xAxisLabelFractionDigits: number
|
|
53
|
+
xAxisLabelTimeZone: DemoXAxisLabelTimeZone
|
|
54
|
+
xAxisLabelTimeZoneOptions: Array<SelectOption<DemoXAxisLabelTimeZone>>
|
|
55
|
+
xAxisLabelShowMilliseconds: boolean
|
|
47
56
|
frameBorderColor: string
|
|
48
57
|
frameBackgroundColor: string
|
|
49
58
|
frameBorderWidth: number
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { computed, ref } from 'vue'
|
|
2
|
+
|
|
3
|
+
import type { WaveformXAxisLabelFormatter } from '../types'
|
|
4
|
+
|
|
5
|
+
export type DemoXAxisLabelFormat = 'number' | 'datetime'
|
|
6
|
+
export type DemoXAxisLabelTimeZone = 'local' | 'Asia/Shanghai' | 'UTC'
|
|
7
|
+
|
|
8
|
+
const formatOptions = [
|
|
9
|
+
{ label: '数值', value: 'number' as const },
|
|
10
|
+
{ label: '固定时间', value: 'datetime' as const },
|
|
11
|
+
]
|
|
12
|
+
const timeZoneOptions = [
|
|
13
|
+
{ label: '中国标准时间', value: 'Asia/Shanghai' as const },
|
|
14
|
+
{ label: '本地时区', value: 'local' as const },
|
|
15
|
+
{ label: 'UTC', value: 'UTC' as const },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
function pad(value: number, length = 2): string {
|
|
19
|
+
return String(value).padStart(length, '0')
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDemoTimestamp(
|
|
23
|
+
value: number,
|
|
24
|
+
timeZone: DemoXAxisLabelTimeZone,
|
|
25
|
+
showMilliseconds: boolean,
|
|
26
|
+
): string {
|
|
27
|
+
const offset = timeZone === 'Asia/Shanghai' ? 8 * 60 * 60 * 1000 : 0
|
|
28
|
+
const date = new Date(value + offset)
|
|
29
|
+
if (!Number.isFinite(date.getTime())) return String(value)
|
|
30
|
+
|
|
31
|
+
const useUtcFields = timeZone !== 'local'
|
|
32
|
+
const year = useUtcFields ? date.getUTCFullYear() : date.getFullYear()
|
|
33
|
+
const month = (useUtcFields ? date.getUTCMonth() : date.getMonth()) + 1
|
|
34
|
+
const day = useUtcFields ? date.getUTCDate() : date.getDate()
|
|
35
|
+
const hours = useUtcFields ? date.getUTCHours() : date.getHours()
|
|
36
|
+
const minutes = useUtcFields ? date.getUTCMinutes() : date.getMinutes()
|
|
37
|
+
const seconds = useUtcFields ? date.getUTCSeconds() : date.getSeconds()
|
|
38
|
+
const milliseconds = useUtcFields ? date.getUTCMilliseconds() : date.getMilliseconds()
|
|
39
|
+
const suffix = showMilliseconds ? `.${pad(milliseconds, 3)}` : ''
|
|
40
|
+
return `${year}-${pad(month)}-${pad(day)} ${pad(hours)}:${pad(minutes)}:${pad(seconds)}${suffix}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function useDemoXAxisLabelControls() {
|
|
44
|
+
const xAxisLabelFormatterEnabled = ref(false)
|
|
45
|
+
const xAxisLabelFormat = ref<DemoXAxisLabelFormat>('number')
|
|
46
|
+
const xAxisLabelMultiplier = ref(1)
|
|
47
|
+
const xAxisLabelFractionDigits = ref(3)
|
|
48
|
+
const xAxisLabelTimeZone = ref<DemoXAxisLabelTimeZone>('Asia/Shanghai')
|
|
49
|
+
const xAxisLabelShowMilliseconds = ref(false)
|
|
50
|
+
const xAxisLabelFormatter = computed<WaveformXAxisLabelFormatter | undefined>(() => {
|
|
51
|
+
if (!xAxisLabelFormatterEnabled.value) return undefined
|
|
52
|
+
if (xAxisLabelFormat.value === 'datetime') {
|
|
53
|
+
return (value) =>
|
|
54
|
+
formatDemoTimestamp(value, xAxisLabelTimeZone.value, xAxisLabelShowMilliseconds.value)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const multiplier = Number.isFinite(xAxisLabelMultiplier.value) ? xAxisLabelMultiplier.value : 1
|
|
58
|
+
const fractionDigits = Number.isFinite(xAxisLabelFractionDigits.value)
|
|
59
|
+
? Math.min(12, Math.max(0, Math.trunc(xAxisLabelFractionDigits.value)))
|
|
60
|
+
: 0
|
|
61
|
+
return (value) => (value * multiplier).toFixed(fractionDigits)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
controlModel: {
|
|
66
|
+
xAxisLabelFormatterEnabled,
|
|
67
|
+
xAxisLabelFormat,
|
|
68
|
+
xAxisLabelFormatOptions: formatOptions,
|
|
69
|
+
xAxisLabelMultiplier,
|
|
70
|
+
xAxisLabelFractionDigits,
|
|
71
|
+
xAxisLabelTimeZone,
|
|
72
|
+
xAxisLabelTimeZoneOptions: timeZoneOptions,
|
|
73
|
+
xAxisLabelShowMilliseconds,
|
|
74
|
+
},
|
|
75
|
+
xAxisLabelFormatter,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { flushPromises, mount } from '@vue/test-utils'
|
|
2
|
+
import { InputNumber, Select } from 'ant-design-vue'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
|
|
5
|
+
import App from '../App.vue'
|
|
6
|
+
import { WaveformChart } from '../components'
|
|
7
|
+
import { formatDemoTimestamp } from './useDemoXAxisLabelControls'
|
|
8
|
+
|
|
9
|
+
describe('X-axis label demo controls', () => {
|
|
10
|
+
it('formats timestamps in fixed UTC and China-standard-time formats', () => {
|
|
11
|
+
expect(formatDemoTimestamp(0, 'UTC', false)).toBe('1970-01-01 00:00:00')
|
|
12
|
+
expect(formatDemoTimestamp(123, 'UTC', true)).toBe('1970-01-01 00:00:00.123')
|
|
13
|
+
expect(formatDemoTimestamp(0, 'Asia/Shanghai', false)).toBe('1970-01-01 08:00:00')
|
|
14
|
+
expect(formatDemoTimestamp(Number.NaN, 'UTC', false)).toBe('NaN')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('configures numeric and timestamp labels from the sidebar', async () => {
|
|
18
|
+
const wrapper = mount(App)
|
|
19
|
+
await flushPromises()
|
|
20
|
+
const chart = wrapper.getComponent(WaveformChart)
|
|
21
|
+
|
|
22
|
+
expect(chart.props('axes')).toEqual({
|
|
23
|
+
x: { lineVisible: false },
|
|
24
|
+
y: { lineVisible: false },
|
|
25
|
+
})
|
|
26
|
+
expect(wrapper.find('[aria-label="自定义 X 轴 Label"]').exists()).toBe(true)
|
|
27
|
+
expect(wrapper.find('[aria-label="X 轴 Label 运算倍率"]').exists()).toBe(false)
|
|
28
|
+
expect(wrapper.find('[aria-label="X 轴 Label 小数位数"]').exists()).toBe(false)
|
|
29
|
+
expect(wrapper.find('[aria-label="X 轴 Label 格式类型"]').exists()).toBe(false)
|
|
30
|
+
|
|
31
|
+
await wrapper.get('[aria-label="自定义 X 轴 Label"]').trigger('click')
|
|
32
|
+
await flushPromises()
|
|
33
|
+
const inputNumbers = wrapper.findAllComponents(InputNumber)
|
|
34
|
+
const multiplierInput = inputNumbers.find((input) =>
|
|
35
|
+
input.find('[aria-label="X 轴 Label 运算倍率"]').exists(),
|
|
36
|
+
)
|
|
37
|
+
const fractionDigitsInput = inputNumbers.find((input) =>
|
|
38
|
+
input.find('[aria-label="X 轴 Label 小数位数"]').exists(),
|
|
39
|
+
)
|
|
40
|
+
expect(multiplierInput).toBeDefined()
|
|
41
|
+
expect(fractionDigitsInput).toBeDefined()
|
|
42
|
+
expect(multiplierInput?.props('value')).toBe(1)
|
|
43
|
+
expect(fractionDigitsInput?.props('value')).toBe(3)
|
|
44
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/\.000$/)
|
|
45
|
+
|
|
46
|
+
multiplierInput?.vm.$emit('update:value', 2)
|
|
47
|
+
fractionDigitsInput?.vm.$emit('update:value', 2)
|
|
48
|
+
await flushPromises()
|
|
49
|
+
const formatter = chart.props('axes')?.x?.labelFormatter
|
|
50
|
+
expect(formatter).toBeTypeOf('function')
|
|
51
|
+
expect(formatter?.(1.234, {} as never)).toBe('2.47')
|
|
52
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/\.00$/)
|
|
53
|
+
|
|
54
|
+
const formatSelect = wrapper
|
|
55
|
+
.findAllComponents(Select)
|
|
56
|
+
.find((select) => select.find('[aria-label="X 轴 Label 格式类型"]').exists())
|
|
57
|
+
expect(formatSelect?.props('value')).toBe('number')
|
|
58
|
+
formatSelect?.vm.$emit('update:value', 'datetime')
|
|
59
|
+
await flushPromises()
|
|
60
|
+
expect(wrapper.find('[aria-label="X 轴 Label 运算倍率"]').exists()).toBe(false)
|
|
61
|
+
expect(wrapper.find('[aria-label="X 轴 Label 小数位数"]').exists()).toBe(false)
|
|
62
|
+
expect(wrapper.find('[aria-label="X 轴 Label 时区"]').exists()).toBe(true)
|
|
63
|
+
expect(wrapper.find('[aria-label="X 轴 Label 显示毫秒"]').exists()).toBe(true)
|
|
64
|
+
expect(chart.props('axes')?.x?.labelFormatter?.(0, {} as never)).toBe('1970-01-01 08:00:00')
|
|
65
|
+
|
|
66
|
+
await wrapper.get('[aria-label="X 轴 Label 显示毫秒"]').trigger('click')
|
|
67
|
+
await flushPromises()
|
|
68
|
+
expect(chart.props('axes')?.x?.labelFormatter?.(123, {} as never)).toBe(
|
|
69
|
+
'1970-01-01 08:00:00.123',
|
|
70
|
+
)
|
|
71
|
+
const timeZoneSelect = wrapper
|
|
72
|
+
.findAllComponents(Select)
|
|
73
|
+
.find((select) => select.find('[aria-label="X 轴 Label 时区"]').exists())
|
|
74
|
+
timeZoneSelect?.vm.$emit('update:value', 'UTC')
|
|
75
|
+
await flushPromises()
|
|
76
|
+
expect(chart.props('axes')?.x?.labelFormatter?.(123, {} as never)).toBe(
|
|
77
|
+
'1970-01-01 00:00:00.123',
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
await wrapper.get('[aria-label="自定义 X 轴 Label"]').trigger('click')
|
|
81
|
+
await flushPromises()
|
|
82
|
+
expect(chart.props('axes')?.x?.labelFormatter).toBeUndefined()
|
|
83
|
+
expect(wrapper.find('[aria-label="X 轴 Label 运算倍率"]').exists()).toBe(false)
|
|
84
|
+
wrapper.unmount()
|
|
85
|
+
})
|
|
86
|
+
})
|