waveform-analysis 0.1.39 → 0.1.41
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 +42 -3
- package/dist/index.cjs +1 -1
- package/dist/index.js +464 -464
- package/dist/types/components/core/layout.d.ts +4 -3
- package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
- package/dist/types/types/chart.d.ts +2 -0
- package/dist/types/utils/formatters.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/core/fixedYDomainLayout.test.ts +75 -5
- package/src/components/core/layout.test.ts +10 -2
- package/src/components/core/layout.ts +25 -9
- package/src/components/core/trackLayoutBuilder.ts +16 -6
- package/src/components/core/useWaveformLayout.ts +12 -1
- package/src/components/rendering/WaveformTrackAxes.vue +2 -1
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +2 -2
- package/src/components/waveformChartCases/dataLifecycle.test.ts +8 -2
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +23 -3
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -1
- package/src/types/chart.ts +2 -0
- package/src/utils/formatters.test.ts +9 -0
- package/src/utils/formatters.ts +6 -3
|
@@ -2,6 +2,7 @@ import type { WaveformOverlayMode } from '../../types';
|
|
|
2
2
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
|
|
3
3
|
import { type WaveformYDomain } from './yDomain';
|
|
4
4
|
export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
|
|
5
|
+
export declare function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number;
|
|
5
6
|
export interface YAxisSeriesGroup {
|
|
6
7
|
index: number;
|
|
7
8
|
side: 'left' | 'right';
|
|
@@ -11,11 +12,11 @@ export interface YAxisSeriesGroup {
|
|
|
11
12
|
}
|
|
12
13
|
export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
|
|
13
14
|
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, tickValues?: number[]): {
|
|
15
|
+
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
|
|
15
16
|
tickTextWidth: number;
|
|
16
17
|
};
|
|
17
|
-
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
|
|
18
|
-
export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain
|
|
18
|
+
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number;
|
|
19
|
+
export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number): {
|
|
19
20
|
left: number;
|
|
20
21
|
right: number;
|
|
21
22
|
};
|
|
@@ -21,6 +21,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
21
21
|
yDomains?: Record<string, [number, number]>;
|
|
22
22
|
timeUnit: 's' | 'ms';
|
|
23
23
|
xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
|
|
24
|
+
yAxisSplitNumber?: number;
|
|
24
25
|
rendering: ResolvedWaveformRenderingOptions;
|
|
25
26
|
hideSecondaryLabels: boolean;
|
|
26
27
|
yAxisLabelX: number;
|
|
@@ -150,6 +150,8 @@ export interface WaveformAxesOptions {
|
|
|
150
150
|
};
|
|
151
151
|
y?: {
|
|
152
152
|
lineVisible?: boolean;
|
|
153
|
+
/** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
|
|
154
|
+
splitNumber?: number;
|
|
153
155
|
};
|
|
154
156
|
}
|
|
155
157
|
/** Styling and visibility options for the horizontal zero-value reference line. */
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ function track(seriesList: DisplaySeries[]): DisplayTrack {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
describe('fixed Y-domain layout', () => {
|
|
38
|
-
it('
|
|
38
|
+
it('expands a global fixed domain to nice equal intervals', () => {
|
|
39
39
|
const sourceTrack = track([series('a', 0, 100)])
|
|
40
40
|
const result = buildTrackLayouts({
|
|
41
41
|
cells: [
|
|
@@ -66,10 +66,80 @@ describe('fixed Y-domain layout', () => {
|
|
|
66
66
|
showCompactEmptyTracks: false,
|
|
67
67
|
})[0]
|
|
68
68
|
|
|
69
|
-
expect(result?.yScale.domain()).toEqual([
|
|
70
|
-
expect(result?.yAxes[0]?.tickValues).toContain(
|
|
71
|
-
expect(result?.yAxes[0]?.tickValues).toContain(
|
|
72
|
-
expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([
|
|
69
|
+
expect(result?.yScale.domain()).toEqual([0, 100])
|
|
70
|
+
expect(result?.yAxes[0]?.tickValues).toContain(0)
|
|
71
|
+
expect(result?.yAxes[0]?.tickValues).toContain(100)
|
|
72
|
+
expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([0, 100])
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('uses the configured split number for fixed domains', () => {
|
|
76
|
+
const result = buildTrackLayouts({
|
|
77
|
+
cells: [
|
|
78
|
+
{
|
|
79
|
+
slotIndex: 0,
|
|
80
|
+
row: 0,
|
|
81
|
+
column: 0,
|
|
82
|
+
left: 0,
|
|
83
|
+
top: 0,
|
|
84
|
+
width: 120,
|
|
85
|
+
height: 100,
|
|
86
|
+
plotHeight: 100,
|
|
87
|
+
cellHeight: 130,
|
|
88
|
+
xAxisBand: 30,
|
|
89
|
+
series: track([series('a', 0, 100)]),
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
|
93
|
+
displayMode: 'independent',
|
|
94
|
+
overlayMode: 'single-axis',
|
|
95
|
+
independentTransforms: [zoomIdentity],
|
|
96
|
+
sharedZoomDomain: [0, 1],
|
|
97
|
+
fixedYDomain: [3, 97],
|
|
98
|
+
yAxisSplitNumber: 5,
|
|
99
|
+
timeUnit: 'ms',
|
|
100
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
101
|
+
hideSecondaryLabels: false,
|
|
102
|
+
yAxisLabelX: -50,
|
|
103
|
+
showCompactEmptyTracks: false,
|
|
104
|
+
})[0]
|
|
105
|
+
|
|
106
|
+
expect(result?.yAxes[0]?.majorTicks).toEqual([0, 25, 50, 75, 100])
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('defaults to five ticks and supports a two-tick axis', () => {
|
|
110
|
+
const build = (splitNumber?: number) =>
|
|
111
|
+
buildTrackLayouts({
|
|
112
|
+
cells: [
|
|
113
|
+
{
|
|
114
|
+
slotIndex: 0,
|
|
115
|
+
row: 0,
|
|
116
|
+
column: 0,
|
|
117
|
+
left: 0,
|
|
118
|
+
top: 0,
|
|
119
|
+
width: 120,
|
|
120
|
+
height: 100,
|
|
121
|
+
plotHeight: 100,
|
|
122
|
+
cellHeight: 130,
|
|
123
|
+
xAxisBand: 30,
|
|
124
|
+
series: track([series('a', 3, 97)]),
|
|
125
|
+
},
|
|
126
|
+
],
|
|
127
|
+
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
|
128
|
+
displayMode: 'independent',
|
|
129
|
+
overlayMode: 'single-axis',
|
|
130
|
+
independentTransforms: [zoomIdentity],
|
|
131
|
+
sharedZoomDomain: [0, 1],
|
|
132
|
+
fixedYDomain: [3, 97],
|
|
133
|
+
yAxisSplitNumber: splitNumber,
|
|
134
|
+
timeUnit: 'ms',
|
|
135
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
136
|
+
hideSecondaryLabels: false,
|
|
137
|
+
yAxisLabelX: -50,
|
|
138
|
+
showCompactEmptyTracks: false,
|
|
139
|
+
})[0]?.yAxes[0]?.majorTicks
|
|
140
|
+
|
|
141
|
+
expect(build()).toHaveLength(5)
|
|
142
|
+
expect(build(2)).toEqual([0, 100])
|
|
73
143
|
})
|
|
74
144
|
|
|
75
145
|
it('resolves track, series, and global fixed-domain precedence', () => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
findClosestTrackAtPointer,
|
|
10
10
|
MAX_MULTI_Y_AXIS_COUNT,
|
|
11
11
|
measureYAxisGroupClearance,
|
|
12
|
+
resolveYAxisTickCount,
|
|
12
13
|
} from './layout'
|
|
13
14
|
|
|
14
15
|
function series(id: string, minimum: number, maximum: number): DisplaySeries {
|
|
@@ -119,8 +120,15 @@ describe('multi-value Y-axis grouping', () => {
|
|
|
119
120
|
showCompactEmptyTracks: false,
|
|
120
121
|
})[0]
|
|
121
122
|
|
|
122
|
-
expect(result?.yScale.domain()).toEqual([
|
|
123
|
-
expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([
|
|
123
|
+
expect(result?.yScale.domain()).toEqual([20, 80])
|
|
124
|
+
expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([20, 80])
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('normalizes configured Y-axis split counts', () => {
|
|
128
|
+
expect(resolveYAxisTickCount(100, 0)).toBe(2)
|
|
129
|
+
expect(resolveYAxisTickCount(100, 4.8)).toBe(4)
|
|
130
|
+
expect(resolveYAxisTickCount(100, Number.NaN)).toBe(5)
|
|
131
|
+
expect(resolveYAxisTickCount(220)).toBe(5)
|
|
124
132
|
})
|
|
125
133
|
|
|
126
134
|
it('keeps every overlaid series on one axis in single-axis mode', () => {
|
|
@@ -20,6 +20,13 @@ const Y_AXIS_OUTER_PADDING = 4
|
|
|
20
20
|
const Y_AXIS_LABEL_GAP = 6
|
|
21
21
|
const Y_AXIS_LABEL_BAND_WIDTH = 24
|
|
22
22
|
|
|
23
|
+
export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number {
|
|
24
|
+
if (typeof splitNumber === 'number' && Number.isFinite(splitNumber)) {
|
|
25
|
+
return Math.max(2, Math.floor(splitNumber))
|
|
26
|
+
}
|
|
27
|
+
return 5
|
|
28
|
+
}
|
|
29
|
+
|
|
23
30
|
export interface YAxisSeriesGroup {
|
|
24
31
|
index: number
|
|
25
32
|
side: 'left' | 'right'
|
|
@@ -103,16 +110,22 @@ export function axisTextMetrics(
|
|
|
103
110
|
domain: [number, number],
|
|
104
111
|
nice = true,
|
|
105
112
|
tickValues?: number[],
|
|
113
|
+
unit?: string,
|
|
114
|
+
tickCount = 10,
|
|
106
115
|
): { tickTextWidth: number } {
|
|
116
|
+
const effectiveTickCount =
|
|
117
|
+
typeof tickCount === 'number' && Number.isFinite(tickCount)
|
|
118
|
+
? Math.max(2, Math.floor(tickCount))
|
|
119
|
+
: 10
|
|
107
120
|
const scale = scaleLinear(domain, [1, 0])
|
|
108
|
-
if (nice) scale.nice()
|
|
121
|
+
if (nice) scale.nice(effectiveTickCount)
|
|
109
122
|
const [axisMin, axisMax] = scale.domain()
|
|
110
|
-
const values = tickValues ??
|
|
123
|
+
const values = tickValues ?? scale.ticks(effectiveTickCount)
|
|
111
124
|
const topTickValue = Math.max(...values)
|
|
112
125
|
const maximumTickCharacters = Math.max(
|
|
113
126
|
1,
|
|
114
127
|
...values.map(
|
|
115
|
-
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
|
|
128
|
+
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue, unit }).length,
|
|
116
129
|
),
|
|
117
130
|
)
|
|
118
131
|
return {
|
|
@@ -120,9 +133,10 @@ export function axisTextMetrics(
|
|
|
120
133
|
}
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
136
|
+
export function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number {
|
|
124
137
|
return (
|
|
125
|
-
axisTextMetrics(group.domain,
|
|
138
|
+
axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
|
|
139
|
+
.tickTextWidth +
|
|
126
140
|
Y_AXIS_TICK_PADDING +
|
|
127
141
|
Y_AXIS_LABEL_GAP +
|
|
128
142
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
@@ -130,9 +144,10 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
|
130
144
|
)
|
|
131
145
|
}
|
|
132
146
|
|
|
133
|
-
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
|
|
147
|
+
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup, tickCount?: number): number {
|
|
134
148
|
return (
|
|
135
|
-
axisTextMetrics(group.domain,
|
|
149
|
+
axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
|
|
150
|
+
.tickTextWidth +
|
|
136
151
|
Y_AXIS_TICK_PADDING +
|
|
137
152
|
Y_AXIS_OUTER_PADDING
|
|
138
153
|
)
|
|
@@ -143,13 +158,14 @@ export function measureTrackYAxisClearance(
|
|
|
143
158
|
overlayMode: WaveformOverlayMode,
|
|
144
159
|
yDomain?: WaveformYDomain,
|
|
145
160
|
yDomains?: Record<string, WaveformYDomain>,
|
|
161
|
+
tickCount?: number,
|
|
146
162
|
): { left: number; right: number } {
|
|
147
163
|
return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).reduce(
|
|
148
164
|
(clearance, group) => {
|
|
149
165
|
clearance[group.side] +=
|
|
150
166
|
overlayMode === 'multi-axis' || track.visibleSeries.length === 1
|
|
151
|
-
? measureYAxisGroupClearance(group)
|
|
152
|
-
: measureYAxisGroupTickClearance(group)
|
|
167
|
+
? measureYAxisGroupClearance(group, tickCount)
|
|
168
|
+
: measureYAxisGroupTickClearance(group, tickCount)
|
|
153
169
|
return clearance
|
|
154
170
|
},
|
|
155
171
|
{ left: 0, right: 0 },
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
type GridCellGeometry,
|
|
26
26
|
type NormalizedWaveformGridOptions,
|
|
27
27
|
} from './grid'
|
|
28
|
-
import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
|
|
28
|
+
import { axisTextMetrics, resolveYAxisSeriesGroups, resolveYAxisTickCount } from './layout'
|
|
29
29
|
import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
|
30
30
|
import { applyXDomainStrategy } from './xDomain'
|
|
31
31
|
import {
|
|
@@ -54,6 +54,7 @@ export interface BuildTrackLayoutsOptions {
|
|
|
54
54
|
yDomains?: Record<string, [number, number]>
|
|
55
55
|
timeUnit: 's' | 'ms'
|
|
56
56
|
xAxisLabelFormatter?: WaveformXAxisLabelFormatter
|
|
57
|
+
yAxisSplitNumber?: number
|
|
57
58
|
rendering: ResolvedWaveformRenderingOptions
|
|
58
59
|
hideSecondaryLabels: boolean
|
|
59
60
|
yAxisLabelX: number
|
|
@@ -124,18 +125,27 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
124
125
|
)
|
|
125
126
|
const sideOffsets = { left: 0, right: 0 }
|
|
126
127
|
const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
|
|
128
|
+
const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
|
|
129
|
+
const niceCount = Math.max(1, tickCount - 1)
|
|
127
130
|
const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
|
|
128
|
-
|
|
129
|
-
const majorTicks = scale.ticks(Math.max(2, Math.floor(cell.plotHeight / 55)))
|
|
131
|
+
scale.nice(niceCount)
|
|
130
132
|
const [axisStart, axisEnd] = scale.domain()
|
|
133
|
+
const majorTicks = Array.from(
|
|
134
|
+
{ length: tickCount },
|
|
135
|
+
(_, index) => axisStart + ((axisEnd - axisStart) * index) / (tickCount - 1),
|
|
136
|
+
)
|
|
131
137
|
const showAxisEnd = options.displayMode !== 'compact' || cell.row === 0
|
|
132
138
|
const visibleMajorTicks = showAxisEnd
|
|
133
139
|
? majorTicks
|
|
134
140
|
: majorTicks.filter((tick) => tick !== axisEnd)
|
|
135
|
-
const tickValues =
|
|
136
|
-
|
|
141
|
+
const tickValues = visibleMajorTicks
|
|
142
|
+
const { tickTextWidth } = axisTextMetrics(
|
|
143
|
+
scale.domain() as [number, number],
|
|
144
|
+
false,
|
|
145
|
+
tickValues,
|
|
146
|
+
group.seriesList[0]?.unit,
|
|
147
|
+
tickCount,
|
|
137
148
|
)
|
|
138
|
-
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
|
139
149
|
const clearance =
|
|
140
150
|
tickTextWidth +
|
|
141
151
|
Y_AXIS_TICK_PADDING +
|
|
@@ -112,7 +112,16 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
112
112
|
.flatMap((track) =>
|
|
113
113
|
resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
|
|
114
114
|
)
|
|
115
|
-
.map(
|
|
115
|
+
.map(
|
|
116
|
+
(group) =>
|
|
117
|
+
axisTextMetrics(
|
|
118
|
+
group.domain,
|
|
119
|
+
true,
|
|
120
|
+
undefined,
|
|
121
|
+
group.seriesList[0]?.unit,
|
|
122
|
+
props.axes?.y?.splitNumber,
|
|
123
|
+
).tickTextWidth,
|
|
124
|
+
)
|
|
116
125
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
117
126
|
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
118
127
|
const labelCenterX = -(
|
|
@@ -157,6 +166,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
157
166
|
props.overlayMode,
|
|
158
167
|
props.yDomain,
|
|
159
168
|
props.yDomains,
|
|
169
|
+
props.axes?.y?.splitNumber,
|
|
160
170
|
)
|
|
161
171
|
return {
|
|
162
172
|
left: Math.max(maximum.left, clearance.left),
|
|
@@ -298,6 +308,7 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
298
308
|
: sharedYDomains.value,
|
|
299
309
|
timeUnit: props.timeUnit,
|
|
300
310
|
xAxisLabelFormatter: props.axes?.x?.labelFormatter,
|
|
311
|
+
yAxisSplitNumber: props.axes?.y?.splitNumber,
|
|
301
312
|
rendering: renderingOptions.value,
|
|
302
313
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
|
303
314
|
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
|
@@ -44,9 +44,10 @@ function renderAxes() {
|
|
|
44
44
|
if (!element) return
|
|
45
45
|
const [axisMin, axisMax] = axis.scale.domain()
|
|
46
46
|
const topTickValue = Math.max(...axis.tickValues)
|
|
47
|
+
const unit = axis.seriesList[0]?.unit
|
|
47
48
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
|
48
49
|
.tickFormat((value) =>
|
|
49
|
-
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue }),
|
|
50
|
+
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue, unit }),
|
|
50
51
|
)
|
|
51
52
|
.tickSize(-4)
|
|
52
53
|
.tickPadding(7)
|
|
@@ -65,9 +65,9 @@ describe('WaveformChart', () => {
|
|
|
65
65
|
)
|
|
66
66
|
|
|
67
67
|
expect(endTicks[0]).toHaveLength(1)
|
|
68
|
-
expect(Number(endTicks[0][0].text())).toBe(
|
|
68
|
+
expect(Number(endTicks[0][0].text())).toBe(1)
|
|
69
69
|
expect(endTicks[1]).toHaveLength(1)
|
|
70
|
-
expect(Number(endTicks[1][0].text())).toBe(
|
|
70
|
+
expect(Number(endTicks[1][0].text())).toBe(2)
|
|
71
71
|
expect(endTicks[2]).toHaveLength(0)
|
|
72
72
|
expect(endTicks[3]).toHaveLength(0)
|
|
73
73
|
})
|
|
@@ -158,6 +158,12 @@ describe('WaveformChart', () => {
|
|
|
158
158
|
expect(paths[0].attributes('data-series-name')).toBe('BT2_2M')
|
|
159
159
|
const yAxisLabels = wrapper.findAll('.waveform-chart__y-axis-label')
|
|
160
160
|
expect(yAxisLabels.map((label) => label.text())).toEqual(['BT2_2M', 'BT1_2M'])
|
|
161
|
+
expect(
|
|
162
|
+
wrapper
|
|
163
|
+
.findAll('.waveform-chart__axis--y .tick text')
|
|
164
|
+
.map((label) => label.text())
|
|
165
|
+
.some((label) => label.includes('(T)')),
|
|
166
|
+
).toBe(true)
|
|
161
167
|
expect(yAxisLabels.map((label) => label.attributes('fill'))).toEqual(['#0960bd', '#ff7f0e'])
|
|
162
168
|
const labelX = Number(
|
|
163
169
|
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
|
@@ -255,7 +261,7 @@ describe('WaveformChart', () => {
|
|
|
255
261
|
})
|
|
256
262
|
|
|
257
263
|
expect(startTicks).toHaveLength(1)
|
|
258
|
-
expect(Number(startTicks[0].text())).toBe(0
|
|
264
|
+
expect(Number(startTicks[0].text())).toBe(0)
|
|
259
265
|
},
|
|
260
266
|
)
|
|
261
267
|
|
|
@@ -291,7 +297,7 @@ describe('WaveformChart', () => {
|
|
|
291
297
|
{ displayMode },
|
|
292
298
|
)
|
|
293
299
|
|
|
294
|
-
const expectedEndValues = [
|
|
300
|
+
const expectedEndValues = [1, 5]
|
|
295
301
|
wrapper.findAll('.waveform-chart__track').forEach((track, index) => {
|
|
296
302
|
const endTicks = track.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
|
|
297
303
|
const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
|
|
@@ -6,6 +6,26 @@ import { resizeObservers } from '../../test/setup'
|
|
|
6
6
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
7
7
|
|
|
8
8
|
describe('WaveformChart', () => {
|
|
9
|
+
it('applies the configured Y-axis split number', async () => {
|
|
10
|
+
const wrapper = await mountSizedChart(
|
|
11
|
+
{
|
|
12
|
+
kind: 'points',
|
|
13
|
+
points: [
|
|
14
|
+
{ x: 0, y: 3 },
|
|
15
|
+
{ x: 1, y: 97 },
|
|
16
|
+
],
|
|
17
|
+
},
|
|
18
|
+
{ axes: { y: { splitNumber: 5 } } },
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
expect(
|
|
22
|
+
wrapper
|
|
23
|
+
.get('.waveform-chart__axis--y')
|
|
24
|
+
.findAll('.tick text')
|
|
25
|
+
.map((tick) => tick.text()),
|
|
26
|
+
).toEqual(['0', '25', '50', '75', '100'])
|
|
27
|
+
})
|
|
28
|
+
|
|
9
29
|
it('expands the Y-axis label gutter for signed values and long exponents', async () => {
|
|
10
30
|
const wrapper = await mountSizedChart(
|
|
11
31
|
{
|
|
@@ -296,7 +316,7 @@ describe('WaveformChart', () => {
|
|
|
296
316
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
297
317
|
})
|
|
298
318
|
|
|
299
|
-
it('reacts to
|
|
319
|
+
it('reacts to nice fixed Y-domain props and returns to automatic bounds', async () => {
|
|
300
320
|
const wrapper = await mountSizedChart({
|
|
301
321
|
kind: 'points',
|
|
302
322
|
points: [
|
|
@@ -312,8 +332,8 @@ describe('WaveformChart', () => {
|
|
|
312
332
|
|
|
313
333
|
await wrapper.setProps({ yDomain: [3, 97] })
|
|
314
334
|
await flushPromises()
|
|
315
|
-
expect(yTickLabels()).toContain('
|
|
316
|
-
expect(yTickLabels()).toContain('
|
|
335
|
+
expect(yTickLabels()).toContain('0')
|
|
336
|
+
expect(yTickLabels()).toContain('100')
|
|
317
337
|
|
|
318
338
|
await wrapper.setProps({ yDomain: undefined })
|
|
319
339
|
await flushPromises()
|
|
@@ -15,6 +15,7 @@ describe('WaveformChart', () => {
|
|
|
15
15
|
{
|
|
16
16
|
trackId: 'shared-frame',
|
|
17
17
|
name: '普通量程',
|
|
18
|
+
unit: 'V',
|
|
18
19
|
data: {
|
|
19
20
|
kind: 'points',
|
|
20
21
|
points: [
|
|
@@ -26,6 +27,7 @@ describe('WaveformChart', () => {
|
|
|
26
27
|
{
|
|
27
28
|
trackId: 'shared-frame',
|
|
28
29
|
name: '大量程',
|
|
30
|
+
unit: 'A',
|
|
29
31
|
data: {
|
|
30
32
|
kind: 'points',
|
|
31
33
|
points: [
|
|
@@ -37,6 +39,7 @@ describe('WaveformChart', () => {
|
|
|
37
39
|
{
|
|
38
40
|
trackId: 'shared-frame',
|
|
39
41
|
name: '小量程',
|
|
42
|
+
unit: 'T',
|
|
40
43
|
data: {
|
|
41
44
|
kind: 'points',
|
|
42
45
|
points: [
|
|
@@ -57,7 +60,11 @@ describe('WaveformChart', () => {
|
|
|
57
60
|
.map((label) => label.text())
|
|
58
61
|
.filter((label) => label.startsWith('E')),
|
|
59
62
|
),
|
|
60
|
-
).toEqual([
|
|
63
|
+
).toEqual([
|
|
64
|
+
[],
|
|
65
|
+
[expect.stringMatching(/^E\+03 \(A\) /)],
|
|
66
|
+
[expect.stringMatching(/^E-04 \(T\) /)],
|
|
67
|
+
])
|
|
61
68
|
})
|
|
62
69
|
|
|
63
70
|
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
package/src/types/chart.ts
CHANGED
|
@@ -40,6 +40,15 @@ describe('waveform number formatters', () => {
|
|
|
40
40
|
expect(formatScientificAxisExponent(0.0001, 0.0003)).toBe('E-04')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
|
+
it('appends a unit to the top tick with or without scientific notation', () => {
|
|
44
|
+
expect(formatScientificAxisLabel(3000, { axisMin: 1000, axisMax: 3000, unit: 'V' })).toBe(
|
|
45
|
+
'E+03 (V) 3',
|
|
46
|
+
)
|
|
47
|
+
expect(formatScientificAxisLabel(3, { axisMin: 0, axisMax: 3, unit: 'A' })).toBe('(A) 3')
|
|
48
|
+
expect(formatScientificAxisLabel(2, { axisMin: 0, axisMax: 3, unit: 'A' })).toBe('2')
|
|
49
|
+
expect(formatScientificAxisLabel(3, { axisMin: 0, axisMax: 3, unit: ' ' })).toBe('3')
|
|
50
|
+
})
|
|
51
|
+
|
|
43
52
|
it('derives the exponent from the largest absolute endpoint', () => {
|
|
44
53
|
expect(resolveScientificAxisExponent(-9000, -1000)).toBe(3)
|
|
45
54
|
expect(resolveScientificAxisExponent(-100_000, -3000)).toBe(5)
|
package/src/utils/formatters.ts
CHANGED
|
@@ -11,6 +11,7 @@ export interface ScientificAxisLabelOptions {
|
|
|
11
11
|
axisMin?: number
|
|
12
12
|
axisMax?: number
|
|
13
13
|
topTickValue?: number
|
|
14
|
+
unit?: string
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
|
@@ -71,9 +72,11 @@ export function formatScientificAxisLabel(
|
|
|
71
72
|
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
|
72
73
|
const formattedValue = formatYAxisNumber(scaledValue)
|
|
73
74
|
const topTickValue = options.topTickValue ?? options.axisMax
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
if (value !== topTickValue) return formattedValue
|
|
76
|
+
const unit = options.unit?.trim()
|
|
77
|
+
const unitLabel = unit ? `(${unit}) ` : ''
|
|
78
|
+
const exponentLabel = exponent === null ? '' : `${formatExponent(exponent)} `
|
|
79
|
+
return `${exponentLabel}${unitLabel}${formattedValue}`
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
/** Return the shared E-style multiplier for an axis, or null for a plain axis. */
|