waveform-analysis 0.1.26 → 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 +2 -2
- package/dist/index.cjs +1 -1
- package/dist/index.js +535 -575
- 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 +4 -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/interaction/useWaveformZoom.ts +12 -8
- 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 +16 -14
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
- package/src/components/waveformChartCases/presentationMode.test.ts +1 -1
- package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +15 -0
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +59 -11
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +114 -0
- 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,
|
|
@@ -86,6 +85,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
86
85
|
? scaleLinear(
|
|
87
86
|
options.initialXDomains?.[displayTrack.id] ??
|
|
88
87
|
options.initialXDomains?.[series.id] ??
|
|
88
|
+
options.initialXDomain ??
|
|
89
89
|
displayTrack.xDomain,
|
|
90
90
|
[0, cell.width],
|
|
91
91
|
)
|
|
@@ -117,31 +117,16 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
117
117
|
const tickValues = Array.from(
|
|
118
118
|
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
|
119
119
|
)
|
|
120
|
-
const {
|
|
121
|
-
group.domain,
|
|
122
|
-
!group.fixed,
|
|
123
|
-
)
|
|
124
|
-
const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
120
|
+
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
|
125
121
|
const clearance =
|
|
126
122
|
tickTextWidth +
|
|
127
123
|
Y_AXIS_TICK_PADDING +
|
|
128
|
-
exponentClearance +
|
|
129
124
|
Y_AXIS_LABEL_GAP +
|
|
130
125
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
131
126
|
Y_AXIS_OUTER_PADDING
|
|
132
127
|
const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
|
|
133
|
-
const exponentX =
|
|
134
|
-
x +
|
|
135
|
-
(group.side === 'left'
|
|
136
|
-
? -(Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
137
|
-
: Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
138
128
|
const labelDistance =
|
|
139
|
-
tickTextWidth +
|
|
140
|
-
Y_AXIS_TICK_PADDING +
|
|
141
|
-
exponentClearance +
|
|
142
|
-
exponentWidth +
|
|
143
|
-
Y_AXIS_LABEL_GAP +
|
|
144
|
-
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
129
|
+
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
145
130
|
const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
|
|
146
131
|
sideOffsets[group.side] += clearance
|
|
147
132
|
return {
|
|
@@ -149,8 +134,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
149
134
|
side: group.side,
|
|
150
135
|
x,
|
|
151
136
|
labelX,
|
|
152
|
-
exponentX,
|
|
153
|
-
exponentLabel,
|
|
154
137
|
scale,
|
|
155
138
|
majorTicks,
|
|
156
139
|
minorTicks: buildMinorTicks(majorTicks),
|
|
@@ -167,7 +150,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
167
150
|
start: formatEndpointTime(domain[0], domain, options.timeUnit),
|
|
168
151
|
end: formatEndpointTime(domain[1], domain, options.timeUnit),
|
|
169
152
|
}
|
|
170
|
-
const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
|
|
171
153
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
|
172
154
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
|
173
155
|
const xAxisTickValues = xMajorTicks.filter((tick) => {
|
|
@@ -234,7 +216,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
234
216
|
yAxisTickValues,
|
|
235
217
|
xAxisTickValues,
|
|
236
218
|
endpointLabels,
|
|
237
|
-
xAxisExponent,
|
|
238
219
|
path: seriesPaths[0]?.path ?? null,
|
|
239
220
|
seriesPaths,
|
|
240
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, {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
select,
|
|
4
4
|
zoom,
|
|
5
5
|
zoomIdentity,
|
|
6
|
+
zoomTransform,
|
|
6
7
|
type D3ZoomEvent,
|
|
7
8
|
type ZoomBehavior,
|
|
8
9
|
type ZoomTransform,
|
|
@@ -237,6 +238,15 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
237
238
|
const tracks = trackLayouts.value.filter((track) => track.hasVisibleSeries)
|
|
238
239
|
return tracks.length > 0 && tracks.every(canZoomTrack)
|
|
239
240
|
}
|
|
241
|
+
const canHandleWheelZoom = (event: Event, canZoomIn: boolean): boolean => {
|
|
242
|
+
if (event.type !== 'wheel') return false
|
|
243
|
+
const deltaY = (event as WheelEvent).deltaY
|
|
244
|
+
if (deltaY < 0) return canZoomIn
|
|
245
|
+
if (deltaY > 0) {
|
|
246
|
+
return zoomTransform(event.currentTarget as Element).k > ZOOM_CONSTRAINTS.MIN_SCALE
|
|
247
|
+
}
|
|
248
|
+
return false
|
|
249
|
+
}
|
|
240
250
|
const configureZoom = () => {
|
|
241
251
|
clearZoomBindings()
|
|
242
252
|
if (
|
|
@@ -256,10 +266,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
256
266
|
if (!overlay) return
|
|
257
267
|
const dataDomain = resolveInitialTrackDomain(track)
|
|
258
268
|
const behavior = zoom<SVGRectElement, unknown>()
|
|
259
|
-
.filter(
|
|
260
|
-
(event) =>
|
|
261
|
-
event.type === 'wheel' && (event as WheelEvent).deltaY < 0 && canZoomTrack(track),
|
|
262
|
-
)
|
|
269
|
+
.filter((event) => canHandleWheelZoom(event, canZoomTrack(track)))
|
|
263
270
|
.scaleExtent([1, resolveMaximumZoomScale(dataDomain)])
|
|
264
271
|
.extent([
|
|
265
272
|
[0, 0],
|
|
@@ -287,10 +294,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|
|
287
294
|
const overlay = sharedOverlayElement.value
|
|
288
295
|
if (!overlay) return
|
|
289
296
|
const behavior = zoom<SVGRectElement, unknown>()
|
|
290
|
-
.filter(
|
|
291
|
-
(event) =>
|
|
292
|
-
event.type === 'wheel' && (event as WheelEvent).deltaY < 0 && canZoomSharedTracks(),
|
|
293
|
-
)
|
|
297
|
+
.filter((event) => canHandleWheelZoom(event, canZoomSharedTracks()))
|
|
294
298
|
.scaleExtent([1, resolveMaximumZoomScale(initialXDomain.value)])
|
|
295
299
|
.extent([
|
|
296
300
|
[0, 0],
|
|
@@ -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 () => {
|