waveform-analysis 0.1.27 → 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 +451 -491
- package/dist/types/components/core/layout.d.ts +2 -4
- package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
- package/dist/types/components/core/types.d.ts +0 -3
- 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 +12 -10
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/App.vue +7 -1
- package/src/components/core/layout.test.ts +7 -8
- package/src/components/core/layout.ts +10 -20
- package/src/components/core/trackLayoutBuilder.ts +24 -26
- package/src/components/core/types.ts +0 -3
- package/src/components/core/useWaveformLayout.ts +6 -27
- package/src/components/interaction/WaveformTooltip.test.ts +16 -0
- package/src/components/rendering/WaveformTrackAxes.vue +10 -29
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +7 -7
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +2 -2
- package/src/components/waveformChartCases/dataLifecycle.test.ts +4 -4
- package/src/components/waveformChartCases/frameGridAndFormatting.test.ts +97 -23
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +15 -15
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +8 -8
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +3 -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 +72 -31
- package/src/utils/formatters.ts +55 -45
- package/src/utils/index.ts +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { WaveformOverlayMode } from '../../types';
|
|
2
2
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
|
|
3
3
|
import { type WaveformYDomain } from './yDomain';
|
|
4
|
-
export { MAX_MULTI_Y_AXIS_COUNT
|
|
4
|
+
export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
|
|
5
5
|
export interface YAxisSeriesGroup {
|
|
6
6
|
index: number;
|
|
7
7
|
side: 'left' | 'right';
|
|
@@ -11,9 +11,7 @@ export interface YAxisSeriesGroup {
|
|
|
11
11
|
}
|
|
12
12
|
export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
|
|
13
13
|
export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
|
|
14
|
-
export declare function axisTextMetrics(domain: [number, number], nice?: boolean): {
|
|
15
|
-
exponentLabel: string | null;
|
|
16
|
-
exponentWidth: number;
|
|
14
|
+
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[]): {
|
|
17
15
|
tickTextWidth: number;
|
|
18
16
|
};
|
|
19
17
|
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
|
|
@@ -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;
|
|
@@ -41,8 +41,6 @@ export interface WaveformYAxisLayout {
|
|
|
41
41
|
side: 'left' | 'right';
|
|
42
42
|
x: number;
|
|
43
43
|
labelX: number;
|
|
44
|
-
exponentX: number;
|
|
45
|
-
exponentLabel: string | null;
|
|
46
44
|
scale: ScaleLinear<number, number>;
|
|
47
45
|
majorTicks: number[];
|
|
48
46
|
minorTicks: number[];
|
|
@@ -102,7 +100,6 @@ export interface TrackLayout {
|
|
|
102
100
|
start: string;
|
|
103
101
|
end: string;
|
|
104
102
|
};
|
|
105
|
-
xAxisExponent: string | null;
|
|
106
103
|
path: string | null;
|
|
107
104
|
seriesPaths: TrackSeriesPath[];
|
|
108
105
|
showXAxis: boolean;
|
|
@@ -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,16 +1,17 @@
|
|
|
1
|
+
import type { WaveformXAxisLabelFormatter, WaveformXAxisLabelKind } from '../types';
|
|
1
2
|
/**
|
|
2
3
|
* 时间单位类型
|
|
3
4
|
*/
|
|
4
5
|
export type TimeUnit = 'ms' | 's';
|
|
5
6
|
export interface ScientificAxisLabelOptions {
|
|
7
|
+
/** @deprecated Y-axis labels always use five significant digits before localization. */
|
|
6
8
|
precision?: number;
|
|
7
9
|
axisMin?: number;
|
|
8
10
|
axisMax?: number;
|
|
11
|
+
topTickValue?: number;
|
|
9
12
|
}
|
|
10
13
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
|
11
|
-
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions
|
|
12
|
-
topTickValue?: number;
|
|
13
|
-
};
|
|
14
|
+
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions;
|
|
14
15
|
/** Whether an axis magnitude should use one shared scientific exponent. */
|
|
15
16
|
export declare function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean;
|
|
16
17
|
export declare function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null;
|
|
@@ -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,22 +44,21 @@ 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
|
-
*
|
|
47
|
+
* 将 X 轴端点时间格式化为当前显示单位下的普通十进制文本
|
|
45
48
|
* @param value 时间值(秒)
|
|
46
|
-
* @param
|
|
49
|
+
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
47
50
|
* @param timeUnit 时间单位
|
|
48
51
|
* @returns 格式化的时间字符串
|
|
49
52
|
*/
|
|
50
|
-
export declare function formatEndpointTime(value: number,
|
|
53
|
+
export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
|
|
51
54
|
/**
|
|
52
|
-
*
|
|
55
|
+
* 将 X 轴时间刻度格式化为当前显示单位下的普通十进制文本
|
|
53
56
|
* @param value 时间值(秒)
|
|
54
57
|
* @param timeUnit 时间单位
|
|
58
|
+
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
55
59
|
* @returns 格式化的时间字符串
|
|
56
60
|
*/
|
|
57
|
-
export declare function formatAxisTime(value: number, timeUnit: TimeUnit,
|
|
58
|
-
/** Return the shared multiplier for an X-axis domain in its selected display unit. */
|
|
59
|
-
export declare function formatAxisTimeExponent(domain: [number, number], timeUnit: TimeUnit): string | null;
|
|
61
|
+
export declare function formatAxisTime(value: number, timeUnit: TimeUnit, _domain?: [number, number]): string;
|
|
60
62
|
/**
|
|
61
63
|
* 格式化悬浮提示时间(4 位小数,本地化格式)
|
|
62
64
|
* @param value 时间值(秒)
|
|
@@ -2,6 +2,6 @@
|
|
|
2
2
|
* 工具函数模块统一导出
|
|
3
3
|
*/
|
|
4
4
|
export { paddedDomain, buildMinorTicks } from './domain';
|
|
5
|
-
export { displayTime, endpointFractionDigits, formatEndpointTime, formatAxisTime,
|
|
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,
|
|
@@ -192,7 +192,7 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
192
192
|
])
|
|
193
193
|
})
|
|
194
194
|
|
|
195
|
-
it('
|
|
195
|
+
it('reserves label clearance for exponent-prefixed ticks on both axis sides', () => {
|
|
196
196
|
const layout = buildTrackLayouts({
|
|
197
197
|
cells: [
|
|
198
198
|
{
|
|
@@ -206,7 +206,7 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
206
206
|
plotHeight: 300,
|
|
207
207
|
cellHeight: 330,
|
|
208
208
|
xAxisBand: 30,
|
|
209
|
-
series: track([series('left',
|
|
209
|
+
series: track([series('left', 1000, 3000), series('right', 1000, 3000)]),
|
|
210
210
|
},
|
|
211
211
|
],
|
|
212
212
|
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
|
@@ -222,14 +222,13 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
222
222
|
})[0]
|
|
223
223
|
|
|
224
224
|
expect(
|
|
225
|
-
layout?.yAxes.map(({ side, x,
|
|
225
|
+
layout?.yAxes.map(({ side, x, labelX }) => ({
|
|
226
226
|
side,
|
|
227
|
-
|
|
228
|
-
exponentLabel,
|
|
227
|
+
labelOffset: Math.abs(labelX - x),
|
|
229
228
|
})),
|
|
230
229
|
).toEqual([
|
|
231
|
-
{ side: 'left',
|
|
232
|
-
{ side: 'right',
|
|
230
|
+
{ side: 'left', labelOffset: 67 },
|
|
231
|
+
{ side: 'right', labelOffset: 67 },
|
|
233
232
|
])
|
|
234
233
|
})
|
|
235
234
|
|
|
@@ -237,7 +236,7 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
237
236
|
const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
|
|
238
237
|
|
|
239
238
|
expect(group).toBeDefined()
|
|
240
|
-
expect(measureYAxisGroupClearance(group!)).toBe(
|
|
239
|
+
expect(measureYAxisGroupClearance(group!)).toBe(90)
|
|
241
240
|
})
|
|
242
241
|
})
|
|
243
242
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { scaleLinear } from 'd3'
|
|
2
2
|
|
|
3
3
|
import type { WaveformOverlayMode } from '../../types'
|
|
4
|
-
import {
|
|
4
|
+
import { formatScientificAxisLabel, paddedDomain } from '../../utils'
|
|
5
5
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
6
|
-
import { MAX_MULTI_Y_AXIS_COUNT
|
|
6
|
+
import { MAX_MULTI_Y_AXIS_COUNT } from './constants'
|
|
7
7
|
import {
|
|
8
8
|
mergeYDomains,
|
|
9
9
|
resolveSeriesFixedYDomain,
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from './yDomain'
|
|
13
13
|
|
|
14
14
|
// 导出常量供外部使用
|
|
15
|
-
export { MAX_MULTI_Y_AXIS_COUNT
|
|
15
|
+
export { MAX_MULTI_Y_AXIS_COUNT } from './constants'
|
|
16
16
|
|
|
17
17
|
const Y_AXIS_CHARACTER_WIDTH = 7
|
|
18
18
|
const Y_AXIS_TICK_PADDING = 7
|
|
@@ -102,36 +102,27 @@ export function resolveYAxisSeriesGroups(
|
|
|
102
102
|
export function axisTextMetrics(
|
|
103
103
|
domain: [number, number],
|
|
104
104
|
nice = true,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
exponentWidth: number
|
|
108
|
-
tickTextWidth: number
|
|
109
|
-
} {
|
|
105
|
+
tickValues?: number[],
|
|
106
|
+
): { tickTextWidth: number } {
|
|
110
107
|
const scale = scaleLinear(domain, [1, 0])
|
|
111
108
|
if (nice) scale.nice()
|
|
112
109
|
const [axisMin, axisMax] = scale.domain()
|
|
113
|
-
const values = scale.ticks(10)
|
|
110
|
+
const values = tickValues ?? Array.from(new Set([axisMin, ...scale.ticks(10), axisMax]))
|
|
111
|
+
const topTickValue = Math.max(...values)
|
|
114
112
|
const maximumTickCharacters = Math.max(
|
|
115
113
|
1,
|
|
116
|
-
...values.map(
|
|
114
|
+
...values.map(
|
|
115
|
+
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
|
|
116
|
+
),
|
|
117
117
|
)
|
|
118
|
-
const exponentLabel = formatScientificAxisExponent(axisMin, axisMax)
|
|
119
118
|
return {
|
|
120
|
-
exponentLabel,
|
|
121
|
-
exponentWidth: exponentLabel ? exponentLabel.length * Y_AXIS_CHARACTER_WIDTH : 0,
|
|
122
119
|
tickTextWidth: maximumTickCharacters * Y_AXIS_CHARACTER_WIDTH,
|
|
123
120
|
}
|
|
124
121
|
}
|
|
125
122
|
|
|
126
|
-
function axisExponentClearance(domain: [number, number], nice: boolean): number {
|
|
127
|
-
const { exponentLabel, exponentWidth } = axisTextMetrics(domain, nice)
|
|
128
|
-
return exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
129
|
-
}
|
|
130
|
-
|
|
131
123
|
export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
132
124
|
return (
|
|
133
125
|
axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
|
|
134
|
-
axisExponentClearance(group.domain, !group.fixed) +
|
|
135
126
|
Y_AXIS_TICK_PADDING +
|
|
136
127
|
Y_AXIS_LABEL_GAP +
|
|
137
128
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
@@ -142,7 +133,6 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
|
142
133
|
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
|
|
143
134
|
return (
|
|
144
135
|
axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
|
|
145
|
-
axisExponentClearance(group.domain, !group.fixed) +
|
|
146
136
|
Y_AXIS_TICK_PADDING +
|
|
147
137
|
Y_AXIS_OUTER_PADDING
|
|
148
138
|
)
|
|
@@ -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,
|
|
@@ -21,7 +26,6 @@ import {
|
|
|
21
26
|
} from './grid'
|
|
22
27
|
import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
|
|
23
28
|
import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
|
24
|
-
import { Y_AXIS_EXPONENT_GAP } from './constants'
|
|
25
29
|
import {
|
|
26
30
|
Y_AXIS_LABEL_BAND_WIDTH,
|
|
27
31
|
Y_AXIS_LABEL_GAP,
|
|
@@ -46,6 +50,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
46
50
|
fixedYDomains?: Record<string, [number, number]>
|
|
47
51
|
yDomains?: Record<string, [number, number]>
|
|
48
52
|
timeUnit: 's' | 'ms'
|
|
53
|
+
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
49
54
|
rendering: ResolvedWaveformRenderingOptions
|
|
50
55
|
hideSecondaryLabels: boolean
|
|
51
56
|
yAxisLabelX: number
|
|
@@ -118,31 +123,16 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
118
123
|
const tickValues = Array.from(
|
|
119
124
|
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
|
120
125
|
)
|
|
121
|
-
const {
|
|
122
|
-
group.domain,
|
|
123
|
-
!group.fixed,
|
|
124
|
-
)
|
|
125
|
-
const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
126
|
+
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
|
126
127
|
const clearance =
|
|
127
128
|
tickTextWidth +
|
|
128
129
|
Y_AXIS_TICK_PADDING +
|
|
129
|
-
exponentClearance +
|
|
130
130
|
Y_AXIS_LABEL_GAP +
|
|
131
131
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
132
132
|
Y_AXIS_OUTER_PADDING
|
|
133
133
|
const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
|
|
134
|
-
const exponentX =
|
|
135
|
-
x +
|
|
136
|
-
(group.side === 'left'
|
|
137
|
-
? -(Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
138
|
-
: Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
139
134
|
const labelDistance =
|
|
140
|
-
tickTextWidth +
|
|
141
|
-
Y_AXIS_TICK_PADDING +
|
|
142
|
-
exponentClearance +
|
|
143
|
-
exponentWidth +
|
|
144
|
-
Y_AXIS_LABEL_GAP +
|
|
145
|
-
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
135
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
146
136
|
const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
|
|
147
137
|
sideOffsets[group.side] += clearance
|
|
148
138
|
return {
|
|
@@ -150,8 +140,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
150
140
|
side: group.side,
|
|
151
141
|
x,
|
|
152
142
|
labelX,
|
|
153
|
-
exponentX,
|
|
154
|
-
exponentLabel,
|
|
155
143
|
scale,
|
|
156
144
|
majorTicks,
|
|
157
145
|
minorTicks: buildMinorTicks(majorTicks),
|
|
@@ -165,10 +153,21 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
165
153
|
const yAxisTickValues = yAxes[0]?.tickValues ?? []
|
|
166
154
|
const domain = xScale.domain() as [number, number]
|
|
167
155
|
const endpointLabels = {
|
|
168
|
-
start:
|
|
169
|
-
|
|
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
|
+
),
|
|
170
170
|
}
|
|
171
|
-
const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
|
|
172
171
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
|
173
172
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
|
174
173
|
const xAxisTickValues = xMajorTicks.filter((tick) => {
|
|
@@ -235,7 +234,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
235
234
|
yAxisTickValues,
|
|
236
235
|
xAxisTickValues,
|
|
237
236
|
endpointLabels,
|
|
238
|
-
xAxisExponent,
|
|
239
237
|
path: seriesPaths[0]?.path ?? null,
|
|
240
238
|
seriesPaths,
|
|
241
239
|
gridLines: options.grid.trackLines[displayTrack.id] ?? {
|
|
@@ -51,8 +51,6 @@ export interface WaveformYAxisLayout {
|
|
|
51
51
|
side: 'left' | 'right'
|
|
52
52
|
x: number
|
|
53
53
|
labelX: number
|
|
54
|
-
exponentX: number
|
|
55
|
-
exponentLabel: string | null
|
|
56
54
|
scale: ScaleLinear<number, number>
|
|
57
55
|
majorTicks: number[]
|
|
58
56
|
minorTicks: number[]
|
|
@@ -109,7 +107,6 @@ export interface TrackLayout {
|
|
|
109
107
|
yAxisTickValues: number[]
|
|
110
108
|
xAxisTickValues: number[]
|
|
111
109
|
endpointLabels: { start: string; end: string }
|
|
112
|
-
xAxisExponent: string | null
|
|
113
110
|
path: string | null
|
|
114
111
|
seriesPaths: TrackSeriesPath[]
|
|
115
112
|
showXAxis: boolean
|
|
@@ -2,7 +2,7 @@ import { scaleLinear, type ZoomTransform } from 'd3'
|
|
|
2
2
|
import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
3
3
|
|
|
4
4
|
import { resolveWaveformRenderingOptions } from '../../core'
|
|
5
|
-
import {
|
|
5
|
+
import { paddedDomain } from '../../utils'
|
|
6
6
|
import {
|
|
7
7
|
layoutAnnotations,
|
|
8
8
|
type AnnotationSeriesInfo,
|
|
@@ -28,9 +28,9 @@ import {
|
|
|
28
28
|
} from './grid'
|
|
29
29
|
import {
|
|
30
30
|
buildTrackLayouts,
|
|
31
|
+
axisTextMetrics,
|
|
31
32
|
measureTrackYAxisClearance,
|
|
32
33
|
resolveYAxisSeriesGroups,
|
|
33
|
-
Y_AXIS_EXPONENT_GAP,
|
|
34
34
|
} from './layout'
|
|
35
35
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
36
36
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
|
@@ -112,40 +112,18 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
112
112
|
.flatMap((track) =>
|
|
113
113
|
resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
|
|
114
114
|
)
|
|
115
|
-
.map((group) =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const [axisMin, axisMax] = scale.domain()
|
|
119
|
-
return {
|
|
120
|
-
exponentLabel: formatScientificAxisExponent(axisMin, axisMax),
|
|
121
|
-
tickLabels: scale
|
|
122
|
-
.ticks(10)
|
|
123
|
-
.map((value) => formatScientificAxisLabel(value, { axisMin, axisMax })),
|
|
124
|
-
}
|
|
125
|
-
})
|
|
126
|
-
const maximumCharacters = Math.max(
|
|
127
|
-
1,
|
|
128
|
-
...axisText.flatMap(({ tickLabels }) => tickLabels).map((label) => label.length),
|
|
129
|
-
)
|
|
130
|
-
const tickTextWidth = maximumCharacters * Y_AXIS_CHARACTER_WIDTH
|
|
131
|
-
const maximumExponentWidth = Math.max(
|
|
132
|
-
0,
|
|
133
|
-
...axisText.map(({ exponentLabel }) => (exponentLabel?.length ?? 0) * Y_AXIS_CHARACTER_WIDTH),
|
|
134
|
-
)
|
|
135
|
-
const exponentClearance = maximumExponentWidth ? maximumExponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
136
|
-
const tickClearance =
|
|
137
|
-
tickTextWidth + Y_AXIS_TICK_PADDING + exponentClearance + Y_AXIS_OUTER_PADDING
|
|
115
|
+
.map((group) => axisTextMetrics(group.domain, !group.fixed).tickTextWidth)
|
|
116
|
+
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
117
|
+
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
138
118
|
const labelCenterX = -(
|
|
139
119
|
Y_AXIS_TICK_PADDING +
|
|
140
120
|
tickTextWidth +
|
|
141
|
-
exponentClearance +
|
|
142
121
|
Y_AXIS_LABEL_GAP +
|
|
143
122
|
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
144
123
|
)
|
|
145
124
|
const fullClearance =
|
|
146
125
|
tickTextWidth +
|
|
147
126
|
Y_AXIS_TICK_PADDING +
|
|
148
|
-
exponentClearance +
|
|
149
127
|
Y_AXIS_LABEL_GAP +
|
|
150
128
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
151
129
|
Y_AXIS_OUTER_PADDING
|
|
@@ -307,6 +285,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
307
285
|
)
|
|
308
286
|
: sharedYDomains.value,
|
|
309
287
|
timeUnit: props.timeUnit,
|
|
288
|
+
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
310
289
|
rendering: renderingOptions.value,
|
|
311
290
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
312
291
|
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
|
@@ -91,6 +91,22 @@ describe('WaveformTooltip', () => {
|
|
|
91
91
|
expect(wrapper.get('.waveform-tooltip__value').text()).toBe('-1,405.4932 A')
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
+
it('formats tooltip time with at most four decimal places', () => {
|
|
95
|
+
const wrapper = mount(WaveformTooltip, {
|
|
96
|
+
props: {
|
|
97
|
+
visible: true,
|
|
98
|
+
position: { x: 10, y: 10 },
|
|
99
|
+
timeUnit: 's',
|
|
100
|
+
hoveredPoint: { x: 1.234567, y: 12 },
|
|
101
|
+
seriesPoints: [],
|
|
102
|
+
containerWidth: 400,
|
|
103
|
+
containerHeight: 300,
|
|
104
|
+
},
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
expect(wrapper.get('.waveform-tooltip__time').text()).toBe('s: 1.2346')
|
|
108
|
+
})
|
|
109
|
+
|
|
94
110
|
it('omits the error label when both resolved errors are zero', () => {
|
|
95
111
|
const point = { x: 1, y: 12 }
|
|
96
112
|
const wrapper = mount(WaveformTooltip, {
|