waveform-analysis 0.1.38 → 0.1.40
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 +43 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +341 -288
- package/dist/types/components/WaveformChart.vue.d.ts +1 -0
- package/dist/types/components/core/layout.d.ts +1 -1
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +3 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
- package/dist/types/components/interaction/viewportDomain.d.ts +21 -0
- package/dist/types/utils/formatters.d.ts +1 -0
- package/package.json +1 -1
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/core/layout.ts +6 -3
- package/src/components/core/trackLayoutBuilder.ts +6 -1
- package/src/components/core/useWaveformChartController.ts +3 -0
- package/src/components/core/useWaveformChartLifecycle.ts +29 -19
- package/src/components/core/useWaveformLayout.ts +5 -1
- package/src/components/interaction/useWaveformViewport.ts +16 -0
- package/src/components/interaction/viewportDomain.ts +60 -0
- package/src/components/rendering/WaveformTrackAxes.vue +2 -1
- package/src/components/waveformChartCases/dataLifecycle.test.ts +6 -0
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -1
- package/src/components/waveformChartCases/viewportReset.test.ts +102 -0
- package/src/utils/formatters.test.ts +9 -0
- package/src/utils/formatters.ts +6 -3
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { WaveformChartProps } from './core/waveformChartTypes';
|
|
2
2
|
declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
|
|
3
3
|
resetViewport: (trackIndex?: number) => void;
|
|
4
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
|
|
4
5
|
}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
|
|
5
6
|
"point-hover": (point: import("..").WaveformPoint | null) => any;
|
|
6
7
|
"zoom-change": (domain: [number, number]) => any;
|
|
@@ -11,7 +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, tickValues?: number[]): {
|
|
14
|
+
export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string): {
|
|
15
15
|
tickTextWidth: number;
|
|
16
16
|
};
|
|
17
17
|
export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
|
|
@@ -228,6 +228,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
|
|
|
228
228
|
finishViewportDrag: (event: PointerEvent) => void;
|
|
229
229
|
cancelViewportDrag: (event?: PointerEvent) => void;
|
|
230
230
|
resetViewport: (trackIndex?: number) => void;
|
|
231
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
|
|
231
232
|
requestViewportReset: (event: MouseEvent) => void;
|
|
232
233
|
toggleSeriesVisibility: (seriesId: string) => void;
|
|
233
234
|
changeDraftSeries: (seriesId: string) => void;
|
|
@@ -31,6 +31,9 @@ interface LifecycleContext {
|
|
|
31
31
|
trackLayouts: ComputedRef<TrackLayout[]>;
|
|
32
32
|
innerWidth: ComputedRef<number>;
|
|
33
33
|
innerHeight: ComputedRef<number>;
|
|
34
|
+
sharedZoomDomain: ComputedRef<[number, number]>;
|
|
35
|
+
initialXDomain: ComputedRef<[number, number]>;
|
|
36
|
+
sharedTransform: ShallowRef<ZoomTransform>;
|
|
34
37
|
activeInteractionMode: ComputedRef<string | undefined>;
|
|
35
38
|
hiddenSeriesIdSet: ComputedRef<Set<string>>;
|
|
36
39
|
internalHiddenSeriesIds: Ref<Set<string>>;
|
|
@@ -42,6 +42,7 @@ export declare function useWaveformViewport(context: ViewportContext): {
|
|
|
42
42
|
finishViewportDrag: (event: PointerEvent) => void;
|
|
43
43
|
cancelViewportDrag: (event?: PointerEvent) => void;
|
|
44
44
|
resetViewport: (trackIndex?: number) => void;
|
|
45
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
|
|
45
46
|
requestViewportReset: (event: MouseEvent) => void;
|
|
46
47
|
};
|
|
47
48
|
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
|
|
2
|
+
import type { ZoomTransform } from 'd3';
|
|
3
|
+
import type { TrackLayout } from '../core/types';
|
|
4
|
+
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes';
|
|
5
|
+
import type { AnnotationSeriesCandidate } from '../annotation';
|
|
6
|
+
interface ViewportDomainContext {
|
|
7
|
+
props: ResolvedWaveformChartProps;
|
|
8
|
+
trackLayouts: ComputedRef<TrackLayout[]>;
|
|
9
|
+
initialXDomain: ComputedRef<[number, number]>;
|
|
10
|
+
resolveInitialTrackDomain: (track: TrackLayout) => [number, number];
|
|
11
|
+
innerWidth: ComputedRef<number>;
|
|
12
|
+
sharedTransform: ShallowRef<ZoomTransform>;
|
|
13
|
+
independentTransforms: ShallowRef<ZoomTransform[]>;
|
|
14
|
+
cancelPendingZoom: () => void;
|
|
15
|
+
cancelViewportDrag: () => void;
|
|
16
|
+
clearHover: () => void;
|
|
17
|
+
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>;
|
|
18
|
+
configureZoom: () => void;
|
|
19
|
+
}
|
|
20
|
+
export declare function createViewportDomainSetter(context: ViewportDomainContext): (domain: [number, number], trackIndex?: number) => void;
|
|
21
|
+
export {};
|
package/package.json
CHANGED
|
@@ -103,6 +103,7 @@ export function axisTextMetrics(
|
|
|
103
103
|
domain: [number, number],
|
|
104
104
|
nice = true,
|
|
105
105
|
tickValues?: number[],
|
|
106
|
+
unit?: string,
|
|
106
107
|
): { tickTextWidth: number } {
|
|
107
108
|
const scale = scaleLinear(domain, [1, 0])
|
|
108
109
|
if (nice) scale.nice()
|
|
@@ -112,7 +113,7 @@ export function axisTextMetrics(
|
|
|
112
113
|
const maximumTickCharacters = Math.max(
|
|
113
114
|
1,
|
|
114
115
|
...values.map(
|
|
115
|
-
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
|
|
116
|
+
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue, unit }).length,
|
|
116
117
|
),
|
|
117
118
|
)
|
|
118
119
|
return {
|
|
@@ -122,7 +123,8 @@ export function axisTextMetrics(
|
|
|
122
123
|
|
|
123
124
|
export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
124
125
|
return (
|
|
125
|
-
axisTextMetrics(group.domain, !group.fixed)
|
|
126
|
+
axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
|
127
|
+
.tickTextWidth +
|
|
126
128
|
Y_AXIS_TICK_PADDING +
|
|
127
129
|
Y_AXIS_LABEL_GAP +
|
|
128
130
|
Y_AXIS_LABEL_BAND_WIDTH +
|
|
@@ -132,7 +134,8 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|
|
132
134
|
|
|
133
135
|
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
|
|
134
136
|
return (
|
|
135
|
-
axisTextMetrics(group.domain, !group.fixed)
|
|
137
|
+
axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
|
138
|
+
.tickTextWidth +
|
|
136
139
|
Y_AXIS_TICK_PADDING +
|
|
137
140
|
Y_AXIS_OUTER_PADDING
|
|
138
141
|
)
|
|
@@ -135,7 +135,12 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|
|
135
135
|
const tickValues = Array.from(
|
|
136
136
|
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
|
137
137
|
)
|
|
138
|
-
const { tickTextWidth } = axisTextMetrics(
|
|
138
|
+
const { tickTextWidth } = axisTextMetrics(
|
|
139
|
+
group.domain,
|
|
140
|
+
!group.fixed,
|
|
141
|
+
tickValues,
|
|
142
|
+
group.seriesList[0]?.unit,
|
|
143
|
+
)
|
|
139
144
|
const clearance =
|
|
140
145
|
tickTextWidth +
|
|
141
146
|
Y_AXIS_TICK_PADDING +
|
|
@@ -39,6 +39,9 @@ interface LifecycleContext {
|
|
|
39
39
|
trackLayouts: ComputedRef<TrackLayout[]>
|
|
40
40
|
innerWidth: ComputedRef<number>
|
|
41
41
|
innerHeight: ComputedRef<number>
|
|
42
|
+
sharedZoomDomain: ComputedRef<[number, number]>
|
|
43
|
+
initialXDomain: ComputedRef<[number, number]>
|
|
44
|
+
sharedTransform: ShallowRef<ZoomTransform>
|
|
42
45
|
activeInteractionMode: ComputedRef<string | undefined>
|
|
43
46
|
hiddenSeriesIdSet: ComputedRef<Set<string>>
|
|
44
47
|
internalHiddenSeriesIds: Ref<Set<string>>
|
|
@@ -93,6 +96,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
93
96
|
trackLayouts,
|
|
94
97
|
innerWidth,
|
|
95
98
|
innerHeight,
|
|
99
|
+
sharedZoomDomain,
|
|
100
|
+
initialXDomain,
|
|
101
|
+
sharedTransform,
|
|
96
102
|
activeInteractionMode,
|
|
97
103
|
hiddenSeriesIdSet,
|
|
98
104
|
internalHiddenSeriesIds,
|
|
@@ -144,25 +150,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
144
150
|
emit('page-change', nextPage, pageCount.value)
|
|
145
151
|
}
|
|
146
152
|
|
|
153
|
+
let pendingSharedXDomain: [number, number] | undefined
|
|
147
154
|
let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
|
|
148
155
|
|
|
149
156
|
function handleBeforeDataReferenceChange() {
|
|
150
|
-
if (props.displayMode
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return track.xScale.domain() as [number, number]
|
|
165
|
-
})
|
|
157
|
+
if (props.displayMode === 'independent') {
|
|
158
|
+
pendingSharedXDomain = undefined
|
|
159
|
+
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
|
160
|
+
const current = track.xScale.domain() as [number, number]
|
|
161
|
+
const boundary = resolveInitialTrackDomain(track)
|
|
162
|
+
return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? current : undefined
|
|
163
|
+
})
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
pendingIndependentXDomains = undefined
|
|
167
|
+
const current = sharedZoomDomain.value
|
|
168
|
+
const boundary = initialXDomain.value
|
|
169
|
+
pendingSharedXDomain =
|
|
170
|
+
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
|
|
166
171
|
}
|
|
167
172
|
|
|
168
173
|
function handleDataReferenceChange() {
|
|
@@ -186,10 +191,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
186
191
|
})
|
|
187
192
|
independentTransforms.value = nextTransforms
|
|
188
193
|
pendingIndependentXDomains = undefined
|
|
189
|
-
|
|
190
|
-
|
|
194
|
+
} else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
|
|
195
|
+
const boundary = initialXDomain.value
|
|
196
|
+
const groups = trackLayouts.value
|
|
197
|
+
.filter((track) => track.hasVisibleSeries)
|
|
198
|
+
.map((track) => track.seriesList)
|
|
199
|
+
const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
|
|
200
|
+
sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
|
|
201
|
+
pendingSharedXDomain = undefined
|
|
191
202
|
}
|
|
192
|
-
pendingIndependentXDomains = undefined
|
|
193
203
|
configureZoom()
|
|
194
204
|
})
|
|
195
205
|
}
|
|
@@ -112,7 +112,11 @@ 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(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
|
118
|
+
.tickTextWidth,
|
|
119
|
+
)
|
|
116
120
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
|
117
121
|
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
|
118
122
|
const labelCenterX = -(
|
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
12
12
|
import { tryReleasePointerCapture } from './pointerCapture'
|
|
13
13
|
import { transitionViewportInteraction } from './viewportInteractionState'
|
|
14
|
+
import { createViewportDomainSetter } from './viewportDomain'
|
|
14
15
|
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
|
15
16
|
interface ViewportContext {
|
|
16
17
|
props: ResolvedWaveformChartProps
|
|
@@ -344,6 +345,20 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
344
345
|
editorSeriesOptions.value = []
|
|
345
346
|
void nextTick(configureZoom)
|
|
346
347
|
}
|
|
348
|
+
const setViewportDomain = createViewportDomainSetter({
|
|
349
|
+
props,
|
|
350
|
+
trackLayouts,
|
|
351
|
+
initialXDomain,
|
|
352
|
+
resolveInitialTrackDomain,
|
|
353
|
+
innerWidth,
|
|
354
|
+
sharedTransform,
|
|
355
|
+
independentTransforms,
|
|
356
|
+
cancelPendingZoom,
|
|
357
|
+
cancelViewportDrag,
|
|
358
|
+
clearHover,
|
|
359
|
+
editorSeriesOptions,
|
|
360
|
+
configureZoom,
|
|
361
|
+
})
|
|
347
362
|
const requestViewportReset = (event: MouseEvent) => {
|
|
348
363
|
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
|
349
364
|
if (props.displayMode === 'independent') {
|
|
@@ -372,6 +387,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|
|
372
387
|
finishViewportDrag,
|
|
373
388
|
cancelViewportDrag,
|
|
374
389
|
resetViewport,
|
|
390
|
+
setViewportDomain,
|
|
375
391
|
requestViewportReset,
|
|
376
392
|
}
|
|
377
393
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { nextTick, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
|
2
|
+
import type { ZoomTransform } from 'd3'
|
|
3
|
+
|
|
4
|
+
import type { TrackLayout } from '../core/types'
|
|
5
|
+
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
6
|
+
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
7
|
+
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
|
8
|
+
|
|
9
|
+
interface ViewportDomainContext {
|
|
10
|
+
props: ResolvedWaveformChartProps
|
|
11
|
+
trackLayouts: ComputedRef<TrackLayout[]>
|
|
12
|
+
initialXDomain: ComputedRef<[number, number]>
|
|
13
|
+
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
|
14
|
+
innerWidth: ComputedRef<number>
|
|
15
|
+
sharedTransform: ShallowRef<ZoomTransform>
|
|
16
|
+
independentTransforms: ShallowRef<ZoomTransform[]>
|
|
17
|
+
cancelPendingZoom: () => void
|
|
18
|
+
cancelViewportDrag: () => void
|
|
19
|
+
clearHover: () => void
|
|
20
|
+
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
|
21
|
+
configureZoom: () => void
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function createViewportDomainSetter(context: ViewportDomainContext) {
|
|
25
|
+
return (domain: [number, number], trackIndex?: number) => {
|
|
26
|
+
if (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) || domain[0] === domain[1])
|
|
27
|
+
return
|
|
28
|
+
context.cancelPendingZoom()
|
|
29
|
+
context.cancelViewportDrag()
|
|
30
|
+
if (context.props.displayMode === 'independent') {
|
|
31
|
+
const indexes =
|
|
32
|
+
trackIndex === undefined
|
|
33
|
+
? context.trackLayouts.value.map((track) => track.index)
|
|
34
|
+
: [trackIndex]
|
|
35
|
+
const nextTransforms = [...context.independentTransforms.value]
|
|
36
|
+
indexes.forEach((index) => {
|
|
37
|
+
const track = context.trackLayouts.value.find((item) => item.index === index)
|
|
38
|
+
if (!track) return
|
|
39
|
+
const boundary = context.resolveInitialTrackDomain(track)
|
|
40
|
+
const constrained = constrainZoomDomain(domain, boundary, [track.seriesList], context.props)
|
|
41
|
+
nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
|
|
42
|
+
})
|
|
43
|
+
context.independentTransforms.value = nextTransforms
|
|
44
|
+
} else {
|
|
45
|
+
const boundary = context.initialXDomain.value
|
|
46
|
+
const groups = context.trackLayouts.value
|
|
47
|
+
.filter((track) => track.hasVisibleSeries)
|
|
48
|
+
.map((track) => track.seriesList)
|
|
49
|
+
const constrained = constrainZoomDomain(domain, boundary, groups, context.props)
|
|
50
|
+
context.sharedTransform.value = transformForDomain(
|
|
51
|
+
constrained,
|
|
52
|
+
boundary,
|
|
53
|
+
context.innerWidth.value,
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
context.clearHover()
|
|
57
|
+
context.editorSeriesOptions.value = []
|
|
58
|
+
void nextTick(context.configureZoom)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -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)
|
|
@@ -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],
|
|
@@ -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 () => {
|
|
@@ -119,6 +119,108 @@ describe('WaveformChart viewport reset', () => {
|
|
|
119
119
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
|
120
120
|
})
|
|
121
121
|
|
|
122
|
+
it('keeps a controlled shared viewport when the data reference changes', async () => {
|
|
123
|
+
const wrapper = await mountSizedChart(
|
|
124
|
+
{
|
|
125
|
+
kind: 'points',
|
|
126
|
+
points: [
|
|
127
|
+
{ x: 0, y: 0 },
|
|
128
|
+
{ x: 1, y: 1 },
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{ displayMode: 'separated' },
|
|
132
|
+
)
|
|
133
|
+
const chart = wrapper.vm as unknown as {
|
|
134
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
|
135
|
+
}
|
|
136
|
+
chart.setViewportDomain([0.25, 0.75])
|
|
137
|
+
await flushPromises()
|
|
138
|
+
|
|
139
|
+
const startBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
|
140
|
+
const endBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
|
141
|
+
expect(startBefore).toBeGreaterThan(0)
|
|
142
|
+
expect(endBefore).toBeLessThan(1000)
|
|
143
|
+
|
|
144
|
+
await wrapper.setProps({
|
|
145
|
+
data: {
|
|
146
|
+
kind: 'points',
|
|
147
|
+
points: [
|
|
148
|
+
{ x: 0, y: 10 },
|
|
149
|
+
{ x: 1, y: 11 },
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
await flushPromises()
|
|
154
|
+
|
|
155
|
+
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())).toBeCloseTo(
|
|
156
|
+
startBefore,
|
|
157
|
+
10,
|
|
158
|
+
)
|
|
159
|
+
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())).toBeCloseTo(
|
|
160
|
+
endBefore,
|
|
161
|
+
10,
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('keeps independently controlled track viewports when the data reference changes', async () => {
|
|
166
|
+
const wrapper = await mountSizedChart(gridSeries(2), {
|
|
167
|
+
displayMode: 'independent',
|
|
168
|
+
grid: { rowCount: 1, columnCount: 2 },
|
|
169
|
+
})
|
|
170
|
+
const chart = wrapper.vm as unknown as {
|
|
171
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
|
172
|
+
}
|
|
173
|
+
chart.setViewportDomain([0.2, 0.8], 0)
|
|
174
|
+
chart.setViewportDomain([0.1, 0.9], 1)
|
|
175
|
+
await flushPromises()
|
|
176
|
+
|
|
177
|
+
const endpoints = () =>
|
|
178
|
+
wrapper.findAll('.waveform-chart__track').map((track) => ({
|
|
179
|
+
start: track.get('.waveform-chart__axis-endpoint--start').text(),
|
|
180
|
+
end: track.get('.waveform-chart__axis-endpoint--end').text(),
|
|
181
|
+
}))
|
|
182
|
+
const endpointsBefore = endpoints()
|
|
183
|
+
expect(Number(endpointsBefore[0].start)).toBeGreaterThan(0)
|
|
184
|
+
expect(Number(endpointsBefore[0].end)).toBeLessThan(1000)
|
|
185
|
+
expect(Number(endpointsBefore[1].start)).toBeGreaterThan(0)
|
|
186
|
+
expect(Number(endpointsBefore[1].end)).toBeLessThan(1000)
|
|
187
|
+
|
|
188
|
+
await wrapper.setProps({ data: gridSeries(2) })
|
|
189
|
+
await flushPromises()
|
|
190
|
+
|
|
191
|
+
const endpointsAfter = endpoints()
|
|
192
|
+
endpointsAfter.forEach((endpoint, index) => {
|
|
193
|
+
expect(Number(endpoint.start)).toBeCloseTo(Number(endpointsBefore[index].start), 10)
|
|
194
|
+
expect(Number(endpoint.end)).toBeCloseTo(Number(endpointsBefore[index].end), 10)
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('ignores invalid domains and applies viewport constraints', async () => {
|
|
199
|
+
const wrapper = await mountSizedChart(
|
|
200
|
+
{
|
|
201
|
+
kind: 'points',
|
|
202
|
+
points: [
|
|
203
|
+
{ x: 0, y: 0 },
|
|
204
|
+
{ x: 1, y: 1 },
|
|
205
|
+
],
|
|
206
|
+
},
|
|
207
|
+
{ displayMode: 'separated', minZoomSpan: 0.2 },
|
|
208
|
+
)
|
|
209
|
+
const chart = wrapper.vm as unknown as {
|
|
210
|
+
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
|
211
|
+
}
|
|
212
|
+
chart.setViewportDomain([Number.NaN, 0.5])
|
|
213
|
+
await flushPromises()
|
|
214
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
|
215
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
216
|
+
|
|
217
|
+
chart.setViewportDomain([0.49, 0.51])
|
|
218
|
+
await flushPromises()
|
|
219
|
+
const start = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
|
220
|
+
const end = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
|
221
|
+
expect(end - start).toBeGreaterThanOrEqual(200)
|
|
222
|
+
})
|
|
223
|
+
|
|
122
224
|
it('resets shared and independent explicit domains to their included nice bounds', async () => {
|
|
123
225
|
const shared = await mountSizedChart(
|
|
124
226
|
{
|
|
@@ -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. */
|