waveform-analysis 0.1.47 → 0.1.50
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 +127 -5
- package/dist/assets/workerRuntime-Dt94jbqq.js +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +2701 -1165
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +14 -0
- package/dist/types/components/annotation/WaveformAnnotationEditor.vue.d.ts +0 -1
- package/dist/types/components/annotation/markup.d.ts +5 -6
- package/dist/types/components/annotation/types.d.ts +2 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +19 -0
- package/dist/types/components/core/layout.d.ts +2 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
- package/dist/types/components/core/types.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +1 -0
- package/dist/types/components/core/useWaveformData.d.ts +2 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +19 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +8 -1
- package/dist/types/components/core/workerSamplingClient.d.ts +8 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/zoomConstraints.d.ts +2 -0
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/core/compactSamplePointSource.d.ts +3 -0
- package/dist/types/core/data.d.ts +7 -1
- package/dist/types/core/index.d.ts +2 -1
- package/dist/types/core/rendering.d.ts +3 -1
- package/dist/types/core/renderingOptions.d.ts +11 -1
- package/dist/types/core/samplingReference.d.ts +36 -0
- package/dist/types/core/samplingReferenceValues.d.ts +4 -0
- package/dist/types/core/samplingReferenceVectors.d.ts +17 -0
- package/dist/types/core/wasmSampling.d.ts +48 -0
- package/dist/types/core/waveformPointSource.d.ts +53 -0
- package/dist/types/core/workerSampling/index.d.ts +5 -0
- package/dist/types/core/workerSampling/javascriptBackend.d.ts +3 -0
- package/dist/types/core/workerSampling/multiresolution.d.ts +29 -0
- package/dist/types/core/workerSampling/protocol.d.ts +153 -0
- package/dist/types/core/workerSampling/repository.d.ts +36 -0
- package/dist/types/core/workerSampling/repositoryDataset.d.ts +29 -0
- package/dist/types/core/workerSampling/repositorySupport.d.ts +27 -0
- package/dist/types/core/workerSampling/samplingCache.d.ts +24 -0
- package/dist/types/core/workerSampling/workerRuntime.d.ts +1 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/types/chart.d.ts +58 -0
- package/dist/types/types/data.d.ts +20 -2
- package/dist/types/types/index.d.ts +2 -2
- package/dist/types/views/WasmSamplingDemo.vue.d.ts +3 -0
- package/package.json +5 -4
- package/src/App.test.ts +11 -7
- package/src/App.vue +2 -2
- package/src/DemoRouterApp.vue +1 -0
- package/src/components/WaveformChart.css +2 -0
- package/src/components/annotation/WaveformAnnotationEditor.css +2 -2
- package/src/components/annotation/WaveformAnnotationEditor.vue +8 -4
- package/src/components/annotation/markup.ts +45 -26
- package/src/components/annotation/types.ts +2 -0
- package/src/components/annotation/useWaveformChartAnnotations.ts +12 -15
- package/src/components/core/latestTaskScheduler.test.ts +51 -0
- package/src/components/core/latestTaskScheduler.ts +75 -0
- package/src/components/core/layout.ts +15 -6
- package/src/components/core/trackLayoutBuilder.ts +8 -2
- package/src/components/core/types.ts +2 -0
- package/src/components/core/useWaveformChartController.ts +14 -0
- package/src/components/core/useWaveformData.test.ts +95 -0
- package/src/components/core/useWaveformData.ts +23 -32
- package/src/components/core/useWaveformLayout.ts +16 -10
- package/src/components/core/useWaveformRenderSampling.test.ts +17 -0
- package/src/components/core/useWaveformRenderSampling.ts +394 -0
- package/src/components/core/waveformChartTypes.ts +13 -0
- package/src/components/core/workerSamplingClient.ts +148 -0
- package/src/components/core/yAxisNice.test.ts +174 -0
- package/src/components/data/types.ts +9 -0
- package/src/components/interaction/WaveformTooltip.test.ts +1 -3
- package/src/components/interaction/WaveformTooltip.vue +13 -12
- package/src/components/interaction/useWaveformHover.ts +7 -7
- package/src/components/interaction/zoomConstraints.ts +5 -1
- package/src/components/waveform.ts +9 -0
- package/src/components/waveformChartCases/compactAndSharedHover.test.ts +15 -4
- package/src/components/waveformChartCases/dataAndHoverPerformance.test.ts +11 -6
- package/src/components/waveformChartCases/dataLifecycle.test.ts +1 -3
- package/src/components/waveformChartCases/trackOrder.test.ts +130 -0
- package/src/components/waveformChartCases/workerSamplingIntegration.test.ts +117 -0
- package/src/components/waveformChartCases/xDomainStrategy.test.ts +14 -2
- package/src/core/compactSamplePointSource.ts +76 -0
- package/src/core/data.test.ts +114 -0
- package/src/core/data.ts +177 -75
- package/src/core/index.ts +11 -0
- package/src/core/rendering.test.ts +87 -12
- package/src/core/rendering.ts +35 -1
- package/src/core/renderingOptions.ts +124 -15
- package/src/core/samplingReference.test.ts +124 -0
- package/src/core/samplingReference.ts +243 -0
- package/src/core/samplingReferenceValues.ts +169 -0
- package/src/core/samplingReferenceVectors.ts +46 -0
- package/src/core/wasmSampling.test.ts +205 -0
- package/src/core/wasmSampling.ts +230 -0
- package/src/core/waveformPointSource.ts +395 -0
- package/src/core/workerSampling/index.ts +30 -0
- package/src/core/workerSampling/javascriptBackend.ts +15 -0
- package/src/core/workerSampling/multiresolution.test.ts +69 -0
- package/src/core/workerSampling/multiresolution.ts +300 -0
- package/src/core/workerSampling/protocol.test.ts +34 -0
- package/src/core/workerSampling/protocol.ts +207 -0
- package/src/core/workerSampling/repository.test.ts +400 -0
- package/src/core/workerSampling/repository.ts +400 -0
- package/src/core/workerSampling/repositoryCache.test.ts +163 -0
- package/src/core/workerSampling/repositoryDataset.ts +194 -0
- package/src/core/workerSampling/repositorySupport.ts +225 -0
- package/src/core/workerSampling/repositoryTypedDataset.test.ts +89 -0
- package/src/core/workerSampling/repositoryWasmDataset.test.ts +70 -0
- package/src/core/workerSampling/samplingCache.ts +94 -0
- package/src/core/workerSampling/workerRuntime.ts +40 -0
- package/src/data/simulatedWaveforms.test.ts +19 -0
- package/src/data/simulatedWaveforms.ts +11 -8
- package/src/demo/xAxisLabelControls.test.ts +1 -1
- package/src/demoRoutes.css +177 -0
- package/src/index.ts +18 -0
- package/src/router.ts +5 -0
- package/src/types/chart.ts +65 -0
- package/src/types/data.ts +24 -1
- package/src/types/index.ts +9 -0
- package/src/views/WasmSamplingDemo.test.ts +13 -0
- package/src/views/WasmSamplingDemo.vue +180 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { zoomIdentity } from 'd3'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
|
|
5
|
+
import type { WaveformData } from '../../types'
|
|
6
|
+
import { buildTrackLayouts } from './trackLayoutBuilder'
|
|
7
|
+
import type { DisplaySeries, DisplayTrack } from './types'
|
|
8
|
+
import { prepareWaveformSeries } from './useWaveformData'
|
|
9
|
+
|
|
10
|
+
function displaySeries(id: string, domain: [number, number]): DisplaySeries {
|
|
11
|
+
return {
|
|
12
|
+
id,
|
|
13
|
+
name: id,
|
|
14
|
+
color: '#1677ff',
|
|
15
|
+
lineType: 'linear',
|
|
16
|
+
lineStyle: 'solid',
|
|
17
|
+
pointType: 'none',
|
|
18
|
+
errorBar: { visible: false, width: 1.5, capWidth: 8 },
|
|
19
|
+
points: [
|
|
20
|
+
{ x: 0, y: domain[0] },
|
|
21
|
+
{ x: 1, y: domain[1] },
|
|
22
|
+
],
|
|
23
|
+
xDomain: [0, 1],
|
|
24
|
+
yDomain: domain,
|
|
25
|
+
hasErrorPoints: false,
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function layout(
|
|
30
|
+
series: DisplaySeries[],
|
|
31
|
+
options: { overlayMode?: 'single-axis' | 'multi-axis'; fixedYDomain?: [number, number] } = {},
|
|
32
|
+
) {
|
|
33
|
+
const track: DisplayTrack = {
|
|
34
|
+
id: 'track',
|
|
35
|
+
series,
|
|
36
|
+
visibleSeries: series,
|
|
37
|
+
xDomain: [0, 1],
|
|
38
|
+
yDomain: [
|
|
39
|
+
Math.min(...series.map((item) => item.yDomain[0])),
|
|
40
|
+
Math.max(...series.map((item) => item.yDomain[1])),
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
return buildTrackLayouts({
|
|
44
|
+
cells: [
|
|
45
|
+
{
|
|
46
|
+
slotIndex: 0,
|
|
47
|
+
row: 0,
|
|
48
|
+
column: 0,
|
|
49
|
+
left: 0,
|
|
50
|
+
top: 0,
|
|
51
|
+
width: 120,
|
|
52
|
+
height: 100,
|
|
53
|
+
plotHeight: 100,
|
|
54
|
+
cellHeight: 130,
|
|
55
|
+
xAxisBand: 30,
|
|
56
|
+
series: track,
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
grid: {
|
|
60
|
+
rowCount: 1,
|
|
61
|
+
columnCount: 1,
|
|
62
|
+
showPagination: false,
|
|
63
|
+
fillIncompleteLastRow: false,
|
|
64
|
+
trackLines: {},
|
|
65
|
+
},
|
|
66
|
+
displayMode: 'independent',
|
|
67
|
+
overlayMode: options.overlayMode ?? 'single-axis',
|
|
68
|
+
independentTransforms: [zoomIdentity],
|
|
69
|
+
sharedZoomDomain: [0, 1],
|
|
70
|
+
fixedYDomain: options.fixedYDomain,
|
|
71
|
+
timeUnit: 'ms',
|
|
72
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
73
|
+
hideSecondaryLabels: false,
|
|
74
|
+
yAxisLabelX: -50,
|
|
75
|
+
yAxisNice: false,
|
|
76
|
+
showCompactEmptyTracks: false,
|
|
77
|
+
})[0]!
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
describe('Y-axis nice domains', () => {
|
|
81
|
+
it('defaults to a nice domain and can preserve the actual data endpoints', () => {
|
|
82
|
+
const source = displaySeries('source', [3, 97])
|
|
83
|
+
const track: DisplayTrack = {
|
|
84
|
+
id: 'track',
|
|
85
|
+
series: [source],
|
|
86
|
+
visibleSeries: [source],
|
|
87
|
+
xDomain: [0, 1],
|
|
88
|
+
yDomain: [3, 97],
|
|
89
|
+
}
|
|
90
|
+
const defaults = buildTrackLayouts({
|
|
91
|
+
cells: [
|
|
92
|
+
{
|
|
93
|
+
slotIndex: 0,
|
|
94
|
+
row: 0,
|
|
95
|
+
column: 0,
|
|
96
|
+
left: 0,
|
|
97
|
+
top: 0,
|
|
98
|
+
width: 120,
|
|
99
|
+
height: 100,
|
|
100
|
+
plotHeight: 100,
|
|
101
|
+
cellHeight: 130,
|
|
102
|
+
xAxisBand: 30,
|
|
103
|
+
series: track,
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
grid: {
|
|
107
|
+
rowCount: 1,
|
|
108
|
+
columnCount: 1,
|
|
109
|
+
showPagination: false,
|
|
110
|
+
fillIncompleteLastRow: false,
|
|
111
|
+
trackLines: {},
|
|
112
|
+
},
|
|
113
|
+
displayMode: 'independent',
|
|
114
|
+
overlayMode: 'single-axis',
|
|
115
|
+
independentTransforms: [zoomIdentity],
|
|
116
|
+
sharedZoomDomain: [0, 1],
|
|
117
|
+
timeUnit: 'ms',
|
|
118
|
+
rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
|
|
119
|
+
hideSecondaryLabels: false,
|
|
120
|
+
yAxisLabelX: -50,
|
|
121
|
+
showCompactEmptyTracks: false,
|
|
122
|
+
})[0]!
|
|
123
|
+
|
|
124
|
+
expect(defaults.yScale.domain()).toEqual([0, 100])
|
|
125
|
+
expect(layout([source]).yScale.domain()).toEqual([3, 97])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('merges visible single-axis series but keeps multi-axis series independent', () => {
|
|
129
|
+
const first = displaySeries('first', [3, 40])
|
|
130
|
+
const second = displaySeries('second', [10, 97])
|
|
131
|
+
|
|
132
|
+
expect(layout([first, second]).yScale.domain()).toEqual([3, 97])
|
|
133
|
+
expect(
|
|
134
|
+
layout([first, second], { overlayMode: 'multi-axis' }).yAxes.map((axis) =>
|
|
135
|
+
axis.scale.domain(),
|
|
136
|
+
),
|
|
137
|
+
).toEqual([
|
|
138
|
+
[3, 40],
|
|
139
|
+
[10, 97],
|
|
140
|
+
])
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('keeps explicit domains and constant-data safety ranges unchanged', () => {
|
|
144
|
+
expect(
|
|
145
|
+
layout([displaySeries('source', [3, 97])], { fixedYDomain: [3, 97] }).yScale.domain(),
|
|
146
|
+
).toEqual([3, 97])
|
|
147
|
+
expect(layout([displaySeries('constant', [9.5, 10.5])]).yScale.domain()).toEqual([9.5, 10.5])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('continues to include visible error-bar bounds in the preserved domain', () => {
|
|
151
|
+
const input: WaveformData = {
|
|
152
|
+
kind: 'series',
|
|
153
|
+
series: [
|
|
154
|
+
{
|
|
155
|
+
id: 'error-series',
|
|
156
|
+
name: 'error series',
|
|
157
|
+
errorBar: { visible: true },
|
|
158
|
+
data: {
|
|
159
|
+
kind: 'points',
|
|
160
|
+
points: [
|
|
161
|
+
{ x: 0, y: 3, lowerError: 2 },
|
|
162
|
+
{ x: 1, y: 7, upperError: 4 },
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}
|
|
168
|
+
const [prepared] = prepareWaveformSeries(input)
|
|
169
|
+
const errorSeries: DisplaySeries = { ...prepared!, color: prepared!.color ?? '#1677ff' }
|
|
170
|
+
|
|
171
|
+
expect(prepared?.yDomain).toEqual([1, 11])
|
|
172
|
+
expect(layout([errorSeries]).yScale.domain()).toEqual([1, 11])
|
|
173
|
+
})
|
|
174
|
+
})
|
|
@@ -14,6 +14,12 @@ export type {
|
|
|
14
14
|
WaveformZoomResetPayload,
|
|
15
15
|
WaveformAnnotationStyle,
|
|
16
16
|
WaveformAnnotation,
|
|
17
|
+
WaveformSamplingMode,
|
|
18
|
+
WaveformSamplingStrategy,
|
|
19
|
+
WaveformSamplingOptions,
|
|
20
|
+
WaveformSamplingBackend,
|
|
21
|
+
WaveformSamplingDiagnostics,
|
|
22
|
+
WaveformSamplingError,
|
|
17
23
|
WaveformRenderingOptions,
|
|
18
24
|
WaveformPlotMargin,
|
|
19
25
|
WaveformTitleTextStyle,
|
|
@@ -30,6 +36,9 @@ export type {
|
|
|
30
36
|
WaveformPointType,
|
|
31
37
|
WaveformErrorBarOptions,
|
|
32
38
|
ResolvedWaveformErrorBarOptions,
|
|
39
|
+
WaveformTypedValues,
|
|
40
|
+
TypedSampleData,
|
|
41
|
+
TypedPointData,
|
|
33
42
|
WaveformSeries,
|
|
34
43
|
WaveformData,
|
|
35
44
|
NormalizedWaveformSeries,
|
|
@@ -188,9 +188,7 @@ describe('WaveformTooltip', () => {
|
|
|
188
188
|
},
|
|
189
189
|
})
|
|
190
190
|
|
|
191
|
-
const values = wrapper
|
|
192
|
-
.findAll('.waveform-tooltip__value')
|
|
193
|
-
.map((value) => value.text())
|
|
191
|
+
const values = wrapper.findAll('.waveform-tooltip__value').map((value) => value.text())
|
|
194
192
|
expect(values).toEqual(['(x:1 y:12)', '(x:1.25 y:8)', '无数据'])
|
|
195
193
|
})
|
|
196
194
|
})
|
|
@@ -90,14 +90,10 @@ const tooltipStyle = computed(() => {
|
|
|
90
90
|
...horizontalStyle,
|
|
91
91
|
top: `${Math.max(
|
|
92
92
|
8,
|
|
93
|
-
Math.min(
|
|
94
|
-
props.position.y - 18,
|
|
95
|
-
props.containerHeight - estimateTooltipHeight(maxWidth) - 8,
|
|
96
|
-
),
|
|
93
|
+
Math.min(props.position.y - 18, props.containerHeight - estimateTooltipHeight(maxWidth) - 8),
|
|
97
94
|
)}px`,
|
|
98
95
|
}
|
|
99
96
|
})
|
|
100
|
-
|
|
101
97
|
</script>
|
|
102
98
|
|
|
103
99
|
<template>
|
|
@@ -113,12 +109,15 @@ const tooltipStyle = computed(() => {
|
|
|
113
109
|
>
|
|
114
110
|
<i :style="{ backgroundColor: seriesPoint.color }" />
|
|
115
111
|
<span class="waveform-tooltip__series-content">
|
|
116
|
-
<span class="waveform-tooltip__series-label"
|
|
117
|
-
seriesPoint.name
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
112
|
+
<span class="waveform-tooltip__series-label"
|
|
113
|
+
>{{ seriesPoint.shotNo?.trim() || '未配置炮号' }}: {{ seriesPoint.name
|
|
114
|
+
}}<template v-if="seriesPoint.unit">({{ seriesPoint.unit }})</template></span
|
|
115
|
+
>
|
|
116
|
+
<span v-if="seriesPoint.point" class="waveform-tooltip__value"
|
|
117
|
+
>(x:{{ formatTooltipTime(seriesPoint.point.x, timeUnit) }} y:{{
|
|
118
|
+
formatTooltipNumber(seriesPoint.point.y)
|
|
119
|
+
}})</span
|
|
120
|
+
>
|
|
122
121
|
<span v-else class="waveform-tooltip__value waveform-tooltip__value--missing">无数据</span>
|
|
123
122
|
</span>
|
|
124
123
|
</span>
|
|
@@ -136,7 +135,9 @@ const tooltipStyle = computed(() => {
|
|
|
136
135
|
max-width: min(560px, calc(100% - 16px));
|
|
137
136
|
padding: 9px 12px;
|
|
138
137
|
color: #505050;
|
|
139
|
-
font:
|
|
138
|
+
font:
|
|
139
|
+
14px/1.35 Arial,
|
|
140
|
+
sans-serif;
|
|
140
141
|
pointer-events: none;
|
|
141
142
|
background: #fff;
|
|
142
143
|
border: 1px solid #e3e7eb;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { pointer } from 'd3'
|
|
2
2
|
import type { ComputedRef, Ref, ShallowRef } from 'vue'
|
|
3
3
|
|
|
4
4
|
import type { WaveformPoint } from '../data/types'
|
|
5
|
+
import { pointSourceFromPoints } from '../../core/waveformPointSource'
|
|
5
6
|
import type {
|
|
6
7
|
DisplaySeries,
|
|
7
8
|
HoveredSeriesPoint,
|
|
@@ -28,8 +29,6 @@ interface HoverContext {
|
|
|
28
29
|
resolveTrackAtPointer: (pointerX: number, pointerY: number) => TrackLayout | undefined
|
|
29
30
|
}
|
|
30
31
|
|
|
31
|
-
const pointBisector = bisector<WaveformPoint, number>((point) => point.x)
|
|
32
|
-
|
|
33
32
|
export function useWaveformHover(context: HoverContext) {
|
|
34
33
|
const {
|
|
35
34
|
emit,
|
|
@@ -110,10 +109,11 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
110
109
|
return true
|
|
111
110
|
}
|
|
112
111
|
const nearestPoint = (series: DisplaySeries, xValue: number): WaveformPoint | undefined => {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
112
|
+
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
113
|
+
const first = source.pointAt(0)
|
|
114
|
+
const last = source.pointAt(source.length - 1)
|
|
115
|
+
if (!first || !last || xValue < first.x || xValue > last.x) return undefined
|
|
116
|
+
return source.nearestPoint(xValue)
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
const handleIndependentPointerMove = (event: PointerEvent, trackIndex: number) => {
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { scaleLinear, zoomIdentity, type ZoomTransform } from 'd3'
|
|
2
2
|
|
|
3
3
|
import type { WaveformPoint } from '../../types'
|
|
4
|
+
import { pointSourceFromPoints, type WaveformPointSource } from '../../core/waveformPointSource'
|
|
4
5
|
import { ZOOM_CONSTRAINTS } from '../core/constants'
|
|
5
6
|
|
|
6
7
|
interface PointSeriesSource {
|
|
7
8
|
points: WaveformPoint[]
|
|
9
|
+
source?: WaveformPointSource
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
export type ZoomSeriesGroup = readonly PointSeriesSource[]
|
|
@@ -30,7 +32,9 @@ function uniqueXValues(group: ZoomSeriesGroup, boundary: [number, number]): numb
|
|
|
30
32
|
if (cached) return cached
|
|
31
33
|
const values = new Set<number>()
|
|
32
34
|
for (const series of group) {
|
|
33
|
-
|
|
35
|
+
const source = series.source ?? pointSourceFromPoints(series.points)
|
|
36
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
37
|
+
const point = source.pointAt(index)!
|
|
34
38
|
if (point.x >= boundary[0] && point.x <= boundary[1]) values.add(point.x)
|
|
35
39
|
}
|
|
36
40
|
}
|
|
@@ -12,12 +12,21 @@ export type {
|
|
|
12
12
|
WaveformXDomainStrategy,
|
|
13
13
|
WaveformAnnotationStyle,
|
|
14
14
|
WaveformAnnotation,
|
|
15
|
+
WaveformSamplingMode,
|
|
16
|
+
WaveformSamplingStrategy,
|
|
17
|
+
WaveformSamplingOptions,
|
|
18
|
+
WaveformSamplingBackend,
|
|
19
|
+
WaveformSamplingDiagnostics,
|
|
20
|
+
WaveformSamplingError,
|
|
15
21
|
WaveformRenderingOptions,
|
|
16
22
|
WaveformPlotMargin,
|
|
17
23
|
WaveformTitleTextStyle,
|
|
18
24
|
WaveformTitleOptions,
|
|
19
25
|
WaveformFrameStyle,
|
|
20
26
|
SingleWaveformData,
|
|
27
|
+
WaveformTypedValues,
|
|
28
|
+
TypedSampleData,
|
|
29
|
+
TypedPointData,
|
|
21
30
|
WaveformSeries,
|
|
22
31
|
WaveformLineStyle,
|
|
23
32
|
WaveformData,
|
|
@@ -74,8 +74,7 @@ describe('WaveformChart', () => {
|
|
|
74
74
|
|
|
75
75
|
it('removes the later compact row top tick when the Y-axis end has a floating-point tail', async () => {
|
|
76
76
|
const yDomain: [number, number] = [-1, 0.2]
|
|
77
|
-
const calculatedAxisEnd =
|
|
78
|
-
yDomain[0] + ((yDomain[1] - yDomain[0]) * 4) / 4
|
|
77
|
+
const calculatedAxisEnd = yDomain[0] + ((yDomain[1] - yDomain[0]) * 4) / 4
|
|
79
78
|
expect(calculatedAxisEnd).not.toBe(yDomain[1])
|
|
80
79
|
|
|
81
80
|
const wrapper = await mountSizedChart(
|
|
@@ -84,11 +83,23 @@ describe('WaveformChart', () => {
|
|
|
84
83
|
series: [
|
|
85
84
|
{
|
|
86
85
|
name: 'top',
|
|
87
|
-
data: {
|
|
86
|
+
data: {
|
|
87
|
+
kind: 'points',
|
|
88
|
+
points: [
|
|
89
|
+
{ x: 0, y: -0.8 },
|
|
90
|
+
{ x: 1, y: 0.1 },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
88
93
|
},
|
|
89
94
|
{
|
|
90
95
|
name: 'bottom',
|
|
91
|
-
data: {
|
|
96
|
+
data: {
|
|
97
|
+
kind: 'points',
|
|
98
|
+
points: [
|
|
99
|
+
{ x: 0, y: -0.7 },
|
|
100
|
+
{ x: 1, y: 0 },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
92
103
|
},
|
|
93
104
|
],
|
|
94
105
|
},
|
|
@@ -99,12 +99,17 @@ describe('WaveformChart', () => {
|
|
|
99
99
|
{ rendering: { downsampleThreshold: 1_000, maxPointsPerPixel: 4 } },
|
|
100
100
|
)
|
|
101
101
|
const overlayWidth = Number(wrapper.get('.waveform-chart__overlay').attributes('width'))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
102
|
+
await vi.waitFor(
|
|
103
|
+
() => {
|
|
104
|
+
expect(wrapper.emitted('sampling-complete')?.length).toBeGreaterThan(0)
|
|
105
|
+
const path = wrapper.get('.waveform-chart__line').attributes('d') ?? ''
|
|
106
|
+
const renderedPointCount = path.match(/[ML]/g)?.length ?? 0
|
|
107
|
+
expect(renderedPointCount).toBeGreaterThan(0)
|
|
108
|
+
expect(renderedPointCount).toBeLessThanOrEqual(Math.floor(overlayWidth * 4) + 2)
|
|
109
|
+
expect(path).toContain(',0')
|
|
110
|
+
},
|
|
111
|
+
{ timeout: 5_000 },
|
|
112
|
+
)
|
|
108
113
|
})
|
|
109
114
|
|
|
110
115
|
it('bounds dense decorations by pixel spacing while keeping one SVG path per series', async () => {
|
|
@@ -169,9 +169,7 @@ describe('WaveformChart', () => {
|
|
|
169
169
|
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
|
170
170
|
)
|
|
171
171
|
expect(labelX).toBeLessThan(-46)
|
|
172
|
-
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(
|
|
173
|
-
labelX - 6,
|
|
174
|
-
)
|
|
172
|
+
expect(Number(wrapper.get('.waveform-chart__y-axis-label-bg').attributes('x'))).toBe(labelX - 6)
|
|
175
173
|
expect(yAxisLabels.every((label) => !label.text().includes('(T)'))).toBe(true)
|
|
176
174
|
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
|
177
175
|
expect(
|
|
@@ -5,6 +5,32 @@ import { flushAnimationFrames } from '../../test/setup'
|
|
|
5
5
|
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
6
|
|
|
7
7
|
describe('WaveformChart track order', () => {
|
|
8
|
+
const points = (offset: number) => ({
|
|
9
|
+
kind: 'points' as const,
|
|
10
|
+
points: [
|
|
11
|
+
{ x: 0, y: offset },
|
|
12
|
+
{ x: 1, y: offset + 1 },
|
|
13
|
+
],
|
|
14
|
+
})
|
|
15
|
+
const orderedSeries = (trackIds: string[]) => ({
|
|
16
|
+
kind: 'series' as const,
|
|
17
|
+
series: trackIds.map((trackId, index) => ({
|
|
18
|
+
id: trackId,
|
|
19
|
+
trackId,
|
|
20
|
+
name: trackId,
|
|
21
|
+
data: points(index + 1),
|
|
22
|
+
})),
|
|
23
|
+
})
|
|
24
|
+
const mergedFourTrackSeries = () => ({
|
|
25
|
+
kind: 'series' as const,
|
|
26
|
+
series: [
|
|
27
|
+
{ id: 'frame-1-a', trackId: 'frame-1', name: 'frame-1-a', data: points(1) },
|
|
28
|
+
{ id: 'frame-1-b', trackId: 'frame-1', name: 'frame-1-b', data: points(2) },
|
|
29
|
+
{ id: 'frame-3', trackId: 'frame-3', name: 'frame-3', data: points(3) },
|
|
30
|
+
{ id: 'frame-4', trackId: 'frame-4', name: 'frame-4', data: points(4) },
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
|
|
8
34
|
it('keeps a merged source slot empty without adding legend or tooltip data', async () => {
|
|
9
35
|
const wrapper = await mountSizedChart(
|
|
10
36
|
{
|
|
@@ -128,4 +154,108 @@ describe('WaveformChart track order', () => {
|
|
|
128
154
|
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
|
|
129
155
|
).toEqual(['frame-2', 'frame-1', 'later-a', 'later-b'])
|
|
130
156
|
})
|
|
157
|
+
|
|
158
|
+
it.each(['independent', 'separated', 'compact'] as const)(
|
|
159
|
+
'reclaims merged empty slots in %s mode when incomplete rows fill',
|
|
160
|
+
async (displayMode) => {
|
|
161
|
+
const wrapper = await mountSizedChart(mergedFourTrackSeries(), {
|
|
162
|
+
displayMode,
|
|
163
|
+
grid: {
|
|
164
|
+
rowCount: 4,
|
|
165
|
+
columnCount: 1,
|
|
166
|
+
fillIncompleteLastRow: true,
|
|
167
|
+
trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4'],
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
171
|
+
const heights = tracks.map((track) => Number(track.attributes('data-track-height')))
|
|
172
|
+
|
|
173
|
+
expect(tracks.map((track) => track.attributes('data-track-id'))).toEqual([
|
|
174
|
+
'frame-1',
|
|
175
|
+
'frame-3',
|
|
176
|
+
'frame-4',
|
|
177
|
+
])
|
|
178
|
+
expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(0)
|
|
179
|
+
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(4)
|
|
180
|
+
expect(heights[0]).toBeCloseTo(heights[1] ?? Number.NaN)
|
|
181
|
+
expect(heights[1]).toBeCloseTo(heights[2] ?? Number.NaN)
|
|
182
|
+
expect(wrapper.findAll('.waveform-chart__legend')).toHaveLength(1)
|
|
183
|
+
expect(wrapper.get('.waveform-chart__legend').text()).not.toContain('frame-2')
|
|
184
|
+
},
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
it('reclaims only the current page without pulling tracks from the next page', async () => {
|
|
188
|
+
const wrapper = await mountSizedChart(
|
|
189
|
+
orderedSeries(['frame-1', 'frame-3', 'frame-4', 'frame-5', 'frame-6', 'frame-7', 'frame-8']),
|
|
190
|
+
{
|
|
191
|
+
grid: {
|
|
192
|
+
rowCount: 4,
|
|
193
|
+
columnCount: 1,
|
|
194
|
+
fillIncompleteLastRow: true,
|
|
195
|
+
trackOrder: [
|
|
196
|
+
'frame-1',
|
|
197
|
+
'frame-2',
|
|
198
|
+
'frame-3',
|
|
199
|
+
'frame-4',
|
|
200
|
+
'frame-5',
|
|
201
|
+
'frame-6',
|
|
202
|
+
'frame-7',
|
|
203
|
+
'frame-8',
|
|
204
|
+
],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
)
|
|
208
|
+
const trackIds = () =>
|
|
209
|
+
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id'))
|
|
210
|
+
|
|
211
|
+
expect(trackIds()).toEqual(['frame-1', 'frame-3', 'frame-4'])
|
|
212
|
+
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
213
|
+
expect(trackIds()).toEqual(['frame-5', 'frame-6', 'frame-7', 'frame-8'])
|
|
214
|
+
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 2])
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('retains ordered empty slots when incomplete rows do not fill', async () => {
|
|
218
|
+
const wrapper = await mountSizedChart(orderedSeries(['frame-1', 'frame-3', 'frame-4']), {
|
|
219
|
+
grid: {
|
|
220
|
+
rowCount: 4,
|
|
221
|
+
columnCount: 1,
|
|
222
|
+
fillIncompleteLastRow: false,
|
|
223
|
+
trackOrder: ['frame-1', 'frame-2', 'frame-3', 'frame-4'],
|
|
224
|
+
},
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
expect(
|
|
228
|
+
wrapper.findAll('.waveform-chart__track').map((track) => track.attributes('data-track-id')),
|
|
229
|
+
).toEqual(['frame-1', 'frame-2', 'frame-3', 'frame-4'])
|
|
230
|
+
expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(1)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('keeps an entirely empty page addressable when incomplete rows fill', async () => {
|
|
234
|
+
const wrapper = await mountSizedChart(
|
|
235
|
+
orderedSeries(['frame-1', 'frame-2', 'frame-3', 'frame-4']),
|
|
236
|
+
{
|
|
237
|
+
grid: {
|
|
238
|
+
rowCount: 4,
|
|
239
|
+
columnCount: 1,
|
|
240
|
+
fillIncompleteLastRow: true,
|
|
241
|
+
trackOrder: [
|
|
242
|
+
'frame-1',
|
|
243
|
+
'frame-2',
|
|
244
|
+
'frame-3',
|
|
245
|
+
'frame-4',
|
|
246
|
+
'frame-5',
|
|
247
|
+
'frame-6',
|
|
248
|
+
'frame-7',
|
|
249
|
+
'frame-8',
|
|
250
|
+
],
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
await wrapper.get('.ant-pagination-next button').trigger('click')
|
|
256
|
+
|
|
257
|
+
expect(wrapper.findAll('.waveform-chart__track--empty')).toHaveLength(4)
|
|
258
|
+
expect(wrapper.emitted('page-change')?.at(-1)).toEqual([2, 2])
|
|
259
|
+
expect(wrapper.get('.ant-pagination-item-active').text()).toBe('2')
|
|
260
|
+
})
|
|
131
261
|
})
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { flushPromises } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
|
|
4
|
+
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import { mountSizedChart } from '../../test/waveformChart'
|
|
6
|
+
|
|
7
|
+
function densePoints(count: number) {
|
|
8
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
9
|
+
x: index,
|
|
10
|
+
y: index === count - 1 ? 100 : Math.sin(index / 10),
|
|
11
|
+
}))
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function samplingEvents(count: number, rendering = {}) {
|
|
15
|
+
const wrapper = await mountSizedChart(
|
|
16
|
+
{ kind: 'points', points: densePoints(count) },
|
|
17
|
+
{ rendering, grid: { rowCount: 1, columnCount: 1 } },
|
|
18
|
+
)
|
|
19
|
+
await flushPromises()
|
|
20
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
21
|
+
return wrapper
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function compactSamplingEvents(count: number, rendering = {}) {
|
|
25
|
+
const wrapper = await mountSizedChart(
|
|
26
|
+
{
|
|
27
|
+
kind: 'typed-points',
|
|
28
|
+
x: Float64Array.from({ length: count }, (_, index) => index),
|
|
29
|
+
y: Float32Array.from({ length: count }, (_, index) =>
|
|
30
|
+
index === count - 1 ? 100 : Math.sin(index / 10),
|
|
31
|
+
),
|
|
32
|
+
},
|
|
33
|
+
{ rendering, grid: { rowCount: 1, columnCount: 1 } },
|
|
34
|
+
)
|
|
35
|
+
await flushPromises()
|
|
36
|
+
await new Promise((resolve) => setTimeout(resolve, 120))
|
|
37
|
+
return wrapper
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('WaveformChart worker sampling integration', () => {
|
|
41
|
+
it('uses raw source points at the auto threshold and JavaScript fallback above it', async () => {
|
|
42
|
+
const threshold = await samplingEvents(1_000)
|
|
43
|
+
const sampled = await samplingEvents(1_001)
|
|
44
|
+
|
|
45
|
+
expect(threshold.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
46
|
+
mode: 'auto',
|
|
47
|
+
backend: 'raw',
|
|
48
|
+
visiblePointCount: 1_000,
|
|
49
|
+
renderedPointCount: 1_000,
|
|
50
|
+
})
|
|
51
|
+
expect(sampled.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
52
|
+
mode: 'auto',
|
|
53
|
+
backend: 'javascript',
|
|
54
|
+
visiblePointCount: 1_001,
|
|
55
|
+
})
|
|
56
|
+
const path = sampled.get('.waveform-chart__line').attributes('d') ?? ''
|
|
57
|
+
expect(path.match(/[ML]/g)?.length).toBeLessThan(4_000)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('bypasses worker sampling in raw mode and reports a forced-WASM failure without a fallback', async () => {
|
|
61
|
+
const raw = await samplingEvents(1_001, { sampling: { mode: 'raw' } })
|
|
62
|
+
const wasm = await samplingEvents(1_001, {
|
|
63
|
+
sampling: { mode: 'wasm', wasmFailureFallback: 'error' },
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(raw.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
67
|
+
mode: 'raw',
|
|
68
|
+
backend: 'raw',
|
|
69
|
+
renderedPointCount: 1_001,
|
|
70
|
+
})
|
|
71
|
+
expect(raw.emitted('sampling-error')).toBeUndefined()
|
|
72
|
+
expect(wasm.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
73
|
+
mode: 'wasm',
|
|
74
|
+
backend: 'unavailable',
|
|
75
|
+
})
|
|
76
|
+
expect(wasm.emitted('sampling-error')?.at(-1)?.[0]).toMatchObject({
|
|
77
|
+
mode: 'wasm',
|
|
78
|
+
fallback: 'none',
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('keeps hover bound to complete source data after sampled SVG geometry is applied', async () => {
|
|
83
|
+
const wrapper = await samplingEvents(1_001)
|
|
84
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
85
|
+
const width = Number(overlay.attributes('width'))
|
|
86
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
87
|
+
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
|
88
|
+
})
|
|
89
|
+
overlay.element.dispatchEvent(
|
|
90
|
+
new MouseEvent('pointermove', { clientX: width, clientY: 120, bubbles: true }),
|
|
91
|
+
)
|
|
92
|
+
flushAnimationFrames()
|
|
93
|
+
await flushPromises()
|
|
94
|
+
|
|
95
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1_000, y: 100 }])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('samples dense typed coordinates while retaining the full compact source for hover', async () => {
|
|
99
|
+
const wrapper = await compactSamplingEvents(1_001)
|
|
100
|
+
expect(wrapper.emitted('sampling-complete')?.at(-1)?.[0]).toMatchObject({
|
|
101
|
+
backend: 'javascript',
|
|
102
|
+
sourcePointCount: 1_001,
|
|
103
|
+
visiblePointCount: 1_001,
|
|
104
|
+
})
|
|
105
|
+
const overlay = wrapper.get('.waveform-chart__overlay')
|
|
106
|
+
const width = Number(overlay.attributes('width'))
|
|
107
|
+
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
|
108
|
+
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
|
109
|
+
})
|
|
110
|
+
overlay.element.dispatchEvent(
|
|
111
|
+
new MouseEvent('pointermove', { clientX: width, clientY: 120, bubbles: true }),
|
|
112
|
+
)
|
|
113
|
+
flushAnimationFrames()
|
|
114
|
+
await flushPromises()
|
|
115
|
+
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1_000, y: 100 }])
|
|
116
|
+
})
|
|
117
|
+
})
|