waveform-analysis 0.1.27 → 0.1.28
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 +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +274 -318
- package/dist/types/components/core/layout.d.ts +2 -4
- package/dist/types/components/core/types.d.ts +0 -3
- package/dist/types/utils/formatters.d.ts +9 -10
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/components/core/layout.test.ts +7 -8
- package/src/components/core/layout.ts +10 -20
- package/src/components/core/trackLayoutBuilder.ts +3 -23
- package/src/components/core/types.ts +0 -3
- package/src/components/core/useWaveformLayout.ts +5 -27
- package/src/components/interaction/WaveformTooltip.test.ts +16 -0
- package/src/components/rendering/WaveformTrackAxes.vue +4 -26
- 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 +29 -22
- 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/utils/formatters.test.ts +40 -31
- package/src/utils/formatters.ts +45 -45
- package/src/utils/index.ts +0 -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;
|
|
@@ -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;
|
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
*/
|
|
4
4
|
export type TimeUnit = 'ms' | 's';
|
|
5
5
|
export interface ScientificAxisLabelOptions {
|
|
6
|
+
/** @deprecated Y-axis labels always use five significant digits before localization. */
|
|
6
7
|
precision?: number;
|
|
7
8
|
axisMin?: number;
|
|
8
9
|
axisMax?: number;
|
|
10
|
+
topTickValue?: number;
|
|
9
11
|
}
|
|
10
12
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
|
11
|
-
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions
|
|
12
|
-
topTickValue?: number;
|
|
13
|
-
};
|
|
13
|
+
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions;
|
|
14
14
|
/** Whether an axis magnitude should use one shared scientific exponent. */
|
|
15
15
|
export declare function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean;
|
|
16
16
|
export declare function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null;
|
|
@@ -41,22 +41,21 @@ export declare function displayTime(value: number, timeUnit: TimeUnit): number;
|
|
|
41
41
|
*/
|
|
42
42
|
export declare function endpointFractionDigits(domain: [number, number], timeUnit: TimeUnit): number;
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* 将 X 轴端点时间格式化为当前显示单位下的普通整数
|
|
45
45
|
* @param value 时间值(秒)
|
|
46
|
-
* @param
|
|
46
|
+
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
47
47
|
* @param timeUnit 时间单位
|
|
48
48
|
* @returns 格式化的时间字符串
|
|
49
49
|
*/
|
|
50
|
-
export declare function formatEndpointTime(value: number,
|
|
50
|
+
export declare function formatEndpointTime(value: number, _domain: [number, number], timeUnit: TimeUnit): string;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* 将 X 轴时间刻度格式化为当前显示单位下的普通整数
|
|
53
53
|
* @param value 时间值(秒)
|
|
54
54
|
* @param timeUnit 时间单位
|
|
55
|
+
* @param _domain 数据域(为保持现有调用签名而保留)
|
|
55
56
|
* @returns 格式化的时间字符串
|
|
56
57
|
*/
|
|
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;
|
|
58
|
+
export declare function formatAxisTime(value: number, timeUnit: TimeUnit, _domain?: [number, number]): string;
|
|
60
59
|
/**
|
|
61
60
|
* 格式化悬浮提示时间(4 位小数,本地化格式)
|
|
62
61
|
* @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, 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
|
@@ -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
|
)
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
type ResolvedWaveformRenderingOptions,
|
|
14
14
|
} from '../../core/rendering'
|
|
15
15
|
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
|
|
16
|
-
import { buildMinorTicks,
|
|
16
|
+
import { buildMinorTicks, formatEndpointTime } from '../../utils'
|
|
17
17
|
import {
|
|
18
18
|
getBottomRowCellIndexes,
|
|
19
19
|
type GridCellGeometry,
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
} from './grid'
|
|
22
22
|
import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
|
|
23
23
|
import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
|
24
|
-
import { Y_AXIS_EXPONENT_GAP } from './constants'
|
|
25
24
|
import {
|
|
26
25
|
Y_AXIS_LABEL_BAND_WIDTH,
|
|
27
26
|
Y_AXIS_LABEL_GAP,
|
|
@@ -118,31 +117,16 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
118
117
|
const tickValues = Array.from(
|
|
119
118
|
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
|
120
119
|
)
|
|
121
|
-
const {
|
|
122
|
-
group.domain,
|
|
123
|
-
!group.fixed,
|
|
124
|
-
)
|
|
125
|
-
const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
120
|
+
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
|
126
121
|
const clearance =
|
|
127
122
|
tickTextWidth +
|
|
128
123
|
Y_AXIS_TICK_PADDING +
|
|
129
|
-
exponentClearance +
|
|
130
124
|
Y_AXIS_LABEL_GAP +
|
|
131
125
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
132
126
|
Y_AXIS_OUTER_PADDING
|
|
133
127
|
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
128
|
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
|
|
129
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
146
130
|
const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
|
|
147
131
|
sideOffsets[group.side] += clearance
|
|
148
132
|
return {
|
|
@@ -150,8 +134,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
150
134
|
side: group.side,
|
|
151
135
|
x,
|
|
152
136
|
labelX,
|
|
153
|
-
exponentX,
|
|
154
|
-
exponentLabel,
|
|
155
137
|
scale,
|
|
156
138
|
majorTicks,
|
|
157
139
|
minorTicks: buildMinorTicks(majorTicks),
|
|
@@ -168,7 +150,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
168
150
|
start: formatEndpointTime(domain[0], domain, options.timeUnit),
|
|
169
151
|
end: formatEndpointTime(domain[1], domain, options.timeUnit),
|
|
170
152
|
}
|
|
171
|
-
const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
|
|
172
153
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
|
173
154
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
|
174
155
|
const xAxisTickValues = xMajorTicks.filter((tick) => {
|
|
@@ -235,7 +216,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
235
216
|
yAxisTickValues,
|
|
236
217
|
xAxisTickValues,
|
|
237
218
|
endpointLabels,
|
|
238
|
-
xAxisExponent,
|
|
239
219
|
path: seriesPaths[0]?.path ?? null,
|
|
240
220
|
seriesPaths,
|
|
241
221
|
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
|
|
@@ -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, {
|
|
@@ -43,8 +43,11 @@ function renderAxes() {
|
|
|
43
43
|
const element = yAxisElements.value[index]
|
|
44
44
|
if (!element) return
|
|
45
45
|
const [axisMin, axisMax] = axis.scale.domain()
|
|
46
|
+
const topTickValue = Math.max(...axis.tickValues)
|
|
46
47
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
|
47
|
-
.tickFormat((value) =>
|
|
48
|
+
.tickFormat((value) =>
|
|
49
|
+
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue }),
|
|
50
|
+
)
|
|
48
51
|
.tickSize(-4)
|
|
49
52
|
.tickPadding(7)
|
|
50
53
|
.tickSizeOuter(0)
|
|
@@ -136,17 +139,6 @@ watch(
|
|
|
136
139
|
{{ track.endpointLabels.end }}
|
|
137
140
|
</text>
|
|
138
141
|
</g>
|
|
139
|
-
<text
|
|
140
|
-
v-if="track.showXAxis && track.xAxisExponent"
|
|
141
|
-
class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
|
|
142
|
-
:x="track.width ?? innerWidth"
|
|
143
|
-
:y="track.height + 27"
|
|
144
|
-
text-anchor="end"
|
|
145
|
-
aria-hidden="true"
|
|
146
|
-
>
|
|
147
|
-
{{ track.xAxisExponent }}
|
|
148
|
-
</text>
|
|
149
|
-
|
|
150
142
|
<g
|
|
151
143
|
v-for="axis in track.isEmpty ? [] : track.yAxes"
|
|
152
144
|
:key="`y-axis-${track.index}-${axis.index}`"
|
|
@@ -160,20 +152,6 @@ watch(
|
|
|
160
152
|
:data-y-axis-side="axis.side"
|
|
161
153
|
:transform="`translate(${axis.x}, 0)`"
|
|
162
154
|
/>
|
|
163
|
-
<text
|
|
164
|
-
v-for="axis in track.isEmpty ? [] : track.yAxes.filter((item) => item.exponentLabel)"
|
|
165
|
-
:key="`y-axis-exponent-${track.index}-${axis.index}`"
|
|
166
|
-
class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
|
|
167
|
-
:data-y-axis-index="axis.index"
|
|
168
|
-
:x="axis.exponentX"
|
|
169
|
-
y="0"
|
|
170
|
-
dy="0.32em"
|
|
171
|
-
:text-anchor="axis.side === 'left' ? 'end' : 'start'"
|
|
172
|
-
aria-hidden="true"
|
|
173
|
-
>
|
|
174
|
-
{{ axis.exponentLabel }}
|
|
175
|
-
</text>
|
|
176
|
-
|
|
177
155
|
<g
|
|
178
156
|
v-if="
|
|
179
157
|
!cleanView &&
|
|
@@ -72,7 +72,7 @@ describe('WaveformChart', () => {
|
|
|
72
72
|
expect(endTicks[3]).toHaveLength(0)
|
|
73
73
|
})
|
|
74
74
|
|
|
75
|
-
it('
|
|
75
|
+
it('prefixes one shared exponent to the largest visible tick on every compact Y axis', async () => {
|
|
76
76
|
const wrapper = await mountSizedChart(
|
|
77
77
|
{
|
|
78
78
|
kind: 'series',
|
|
@@ -83,7 +83,7 @@ describe('WaveformChart', () => {
|
|
|
83
83
|
kind: 'points',
|
|
84
84
|
points: [
|
|
85
85
|
{ x: 0, y: 0 },
|
|
86
|
-
{ x: 1, y:
|
|
86
|
+
{ x: 1, y: 2540 },
|
|
87
87
|
],
|
|
88
88
|
},
|
|
89
89
|
},
|
|
@@ -103,14 +103,14 @@ describe('WaveformChart', () => {
|
|
|
103
103
|
)
|
|
104
104
|
|
|
105
105
|
const axes = wrapper.findAll('.waveform-chart__axis--y')
|
|
106
|
-
const exponents = wrapper.findAll('.waveform-chart__axis-exponent--y')
|
|
107
106
|
expect(axes).toHaveLength(2)
|
|
108
|
-
|
|
109
|
-
axes.forEach((axis) => {
|
|
107
|
+
axes.forEach((axis, index) => {
|
|
110
108
|
const labels = axis.findAll('.tick text').map((tick) => tick.text())
|
|
111
|
-
|
|
112
|
-
expect(
|
|
109
|
+
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
|
110
|
+
expect(exponentLabels).toHaveLength(1)
|
|
111
|
+
expect(exponentLabels[0]).toMatch(index === 0 ? /^E\+03 / : /^E-04 /)
|
|
113
112
|
})
|
|
113
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
it('prefers a trimmed series name and falls back to yLabel for unnamed data', async () => {
|
|
@@ -178,7 +178,7 @@ describe('WaveformChart', () => {
|
|
|
178
178
|
await flushPromises()
|
|
179
179
|
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
|
180
180
|
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
|
181
|
-
expect(wrapper.get('.waveform-chart__tooltip').text()).
|
|
181
|
+
expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
|
|
182
182
|
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
|
183
183
|
expect(crosshairLines).toHaveLength(1)
|
|
184
184
|
expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
|
|
@@ -303,7 +303,7 @@ describe('WaveformChart', () => {
|
|
|
303
303
|
flushAnimationFrames()
|
|
304
304
|
await flushPromises()
|
|
305
305
|
|
|
306
|
-
expect(wrapper.get('.waveform-chart__tooltip').text()).
|
|
306
|
+
expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
|
|
307
307
|
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
|
|
308
308
|
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
|
|
309
309
|
expect(chartUpdate).not.toHaveBeenCalled()
|
|
@@ -62,15 +62,15 @@ describe('WaveformChart', () => {
|
|
|
62
62
|
],
|
|
63
63
|
}
|
|
64
64
|
const wrapper = await mountSizedChart(firstData)
|
|
65
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
65
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
66
66
|
|
|
67
67
|
firstData.points.push({ x: 2, y: 2 })
|
|
68
68
|
await flushPromises()
|
|
69
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
69
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
70
70
|
|
|
71
71
|
await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
|
|
72
72
|
await flushPromises()
|
|
73
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
73
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
74
74
|
})
|
|
75
75
|
|
|
76
76
|
it('keeps controlled annotations when replacing the loaded data window', async () => {
|
|
@@ -170,7 +170,7 @@ describe('WaveformChart', () => {
|
|
|
170
170
|
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
|
171
171
|
expect(
|
|
172
172
|
wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
|
|
173
|
-
).toEqual(['
|
|
173
|
+
).toEqual(['1000', '2000'])
|
|
174
174
|
})
|
|
175
175
|
|
|
176
176
|
it('keeps the zero Y-axis label on upper compact tracks', async () => {
|
|
@@ -162,35 +162,42 @@ describe('WaveformChart', () => {
|
|
|
162
162
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
|
163
163
|
String(trackWidth),
|
|
164
164
|
)
|
|
165
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
166
|
-
expect(wrapper.
|
|
165
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
|
|
166
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
167
167
|
})
|
|
168
168
|
|
|
169
169
|
it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
|
|
170
170
|
const cases = [
|
|
171
|
-
{ values: [0, 50], exponent: null },
|
|
172
|
-
{ values: [
|
|
173
|
-
{
|
|
171
|
+
{ values: [0, 50], yDomain: [0, 50] as [number, number], exponent: null },
|
|
172
|
+
{ values: [1000, 3000], yDomain: [1000, 3000] as [number, number], exponent: 'E+03' },
|
|
173
|
+
{
|
|
174
|
+
values: [0.0001, 0.0003],
|
|
175
|
+
yDomain: [0.0001, 0.0003] as [number, number],
|
|
176
|
+
exponent: 'E-04',
|
|
177
|
+
},
|
|
174
178
|
]
|
|
175
179
|
|
|
176
|
-
for (const { values, exponent } of cases) {
|
|
177
|
-
const
|
|
178
|
-
kind: 'points',
|
|
180
|
+
for (const { values, yDomain, exponent } of cases) {
|
|
181
|
+
const data = {
|
|
182
|
+
kind: 'points' as const,
|
|
179
183
|
points: values.map((y, x) => ({ x, y })),
|
|
180
|
-
}
|
|
184
|
+
}
|
|
185
|
+
const originalValues = data.points.map((point) => point.y)
|
|
186
|
+
const wrapper = await mountSizedChart(data, { yDomain })
|
|
181
187
|
const labels = wrapper
|
|
182
188
|
.get('.waveform-chart__axis--y')
|
|
183
189
|
.findAll('.tick text')
|
|
184
190
|
.map((tick) => tick.text())
|
|
185
|
-
const
|
|
191
|
+
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
|
186
192
|
|
|
187
193
|
if (exponent === null) {
|
|
188
|
-
expect(
|
|
194
|
+
expect(exponentLabels).toHaveLength(0)
|
|
189
195
|
} else {
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
192
|
-
expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
|
|
196
|
+
expect(exponentLabels).toHaveLength(1)
|
|
197
|
+
expect(exponentLabels[0]).toMatch(new RegExp(`^${exponent.replace('+', '\\+')} `))
|
|
193
198
|
}
|
|
199
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
200
|
+
expect(data.points.map((point) => point.y)).toEqual(originalValues)
|
|
194
201
|
|
|
195
202
|
wrapper.unmount()
|
|
196
203
|
}
|
|
@@ -212,13 +219,13 @@ describe('WaveformChart', () => {
|
|
|
212
219
|
|
|
213
220
|
expect(millisecondsChart.text()).toContain('时间(ms)')
|
|
214
221
|
expect(millisecondTicks.length).toBeGreaterThan(0)
|
|
215
|
-
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
216
|
-
expect(millisecondsChart.
|
|
222
|
+
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
223
|
+
expect(millisecondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
217
224
|
expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
|
|
218
225
|
|
|
219
226
|
const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
|
|
220
227
|
expect(secondsChart.text()).toContain('Elapsed time')
|
|
221
|
-
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1
|
|
228
|
+
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1')
|
|
222
229
|
expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
223
230
|
})
|
|
224
231
|
|
|
@@ -237,15 +244,15 @@ describe('WaveformChart', () => {
|
|
|
237
244
|
|
|
238
245
|
expect(start.attributes('x')).toBe('0')
|
|
239
246
|
expect(start.attributes('text-anchor')).toBe('start')
|
|
240
|
-
expect(start.text()).toBe('0
|
|
247
|
+
expect(start.text()).toBe('0')
|
|
241
248
|
expect(end.attributes('x')).toBe(
|
|
242
249
|
wrapper.get('.waveform-chart__track').attributes('data-track-width'),
|
|
243
250
|
)
|
|
244
251
|
expect(end.attributes('text-anchor')).toBe('end')
|
|
245
|
-
expect(end.text()).toBe('
|
|
252
|
+
expect(end.text()).toBe('1999')
|
|
246
253
|
expect(middleTickLabels.length).toBeGreaterThan(0)
|
|
247
|
-
expect(middleTickLabels).not.toContain('0
|
|
248
|
-
expect(wrapper.
|
|
254
|
+
expect(middleTickLabels).not.toContain('0')
|
|
255
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
249
256
|
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
|
|
250
257
|
middleTickLabels.length,
|
|
251
258
|
)
|
|
@@ -300,6 +307,6 @@ describe('WaveformChart', () => {
|
|
|
300
307
|
)
|
|
301
308
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
|
302
309
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
|
303
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).
|
|
310
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/^-?\d+$/)
|
|
304
311
|
})
|
|
305
312
|
})
|
|
@@ -59,7 +59,7 @@ describe('WaveformChart', () => {
|
|
|
59
59
|
await flushPromises()
|
|
60
60
|
|
|
61
61
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
|
|
62
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
62
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
it('updates rendering props and disables zoom interaction', async () => {
|