waveform-analysis 0.1.27 → 0.1.29
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 +57 -33
- package/dist/index.cjs +1 -1
- package/dist/index.js +451 -491
- package/dist/types/components/core/layout.d.ts +2 -4
- package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -1
- package/dist/types/components/core/types.d.ts +0 -3
- package/dist/types/demo/types.d.ts +9 -0
- package/dist/types/demo/useDemoXAxisLabelControls.d.ts +32 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +15 -0
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/formatters.d.ts +12 -10
- package/dist/types/utils/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/App.vue +7 -1
- package/src/components/core/layout.test.ts +7 -8
- package/src/components/core/layout.ts +10 -20
- package/src/components/core/trackLayoutBuilder.ts +24 -26
- package/src/components/core/types.ts +0 -3
- package/src/components/core/useWaveformLayout.ts +6 -27
- package/src/components/interaction/WaveformTooltip.test.ts +16 -0
- package/src/components/rendering/WaveformTrackAxes.vue +10 -29
- 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 +97 -23
- package/src/components/waveformChartCases/interactionAndAnnotationCreation.test.ts +1 -1
- package/src/components/waveformChartCases/layoutAndDomains.test.ts +15 -15
- package/src/components/waveformChartCases/multiAxisAndPagination.test.ts +8 -8
- package/src/components/waveformChartCases/presentationAndAxes.test.ts +6 -4
- package/src/components/waveformChartCases/viewportConstraintsAndReset.test.ts +8 -8
- package/src/components/waveformChartCases/wheelZoomOut.test.ts +3 -3
- package/src/demo/DemoGridFrameControls.vue +70 -0
- package/src/demo/types.ts +9 -0
- package/src/demo/useDemoXAxisLabelControls.ts +77 -0
- package/src/demo/xAxisLabelControls.test.ts +86 -0
- package/src/index.ts +3 -0
- package/src/styles.css +8 -0
- package/src/types/chart.ts +21 -0
- package/src/types/index.ts +3 -0
- package/src/utils/formatters.test.ts +72 -31
- package/src/utils/formatters.ts +55 -45
- package/src/utils/index.ts +1 -1
|
@@ -3,7 +3,7 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
|
|
3
3
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
4
4
|
|
|
5
5
|
import type { WaveformAxesOptions } from '../../types'
|
|
6
|
-
import {
|
|
6
|
+
import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
|
|
7
7
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
@@ -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)
|
|
@@ -63,10 +66,12 @@ function renderAxes() {
|
|
|
63
66
|
axisBottom(props.track.xScale)
|
|
64
67
|
.tickValues(props.track.xAxisTickValues)
|
|
65
68
|
.tickFormat((value) =>
|
|
66
|
-
|
|
69
|
+
formatXAxisLabel(
|
|
67
70
|
Number(value),
|
|
68
|
-
props.timeUnit,
|
|
69
71
|
props.track.xScale.domain() as [number, number],
|
|
72
|
+
props.timeUnit,
|
|
73
|
+
'tick',
|
|
74
|
+
props.axes?.x?.labelFormatter,
|
|
70
75
|
),
|
|
71
76
|
)
|
|
72
77
|
.tickSize(-4)
|
|
@@ -90,6 +95,7 @@ watch(
|
|
|
90
95
|
() => props.track.xAxisTickValues,
|
|
91
96
|
() => props.track.yAxisTickValues,
|
|
92
97
|
() => props.timeUnit,
|
|
98
|
+
() => props.axes?.x?.labelFormatter,
|
|
93
99
|
() => props.axes?.x?.lineVisible,
|
|
94
100
|
() => props.axes?.y?.lineVisible,
|
|
95
101
|
],
|
|
@@ -136,17 +142,6 @@ watch(
|
|
|
136
142
|
{{ track.endpointLabels.end }}
|
|
137
143
|
</text>
|
|
138
144
|
</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
145
|
<g
|
|
151
146
|
v-for="axis in track.isEmpty ? [] : track.yAxes"
|
|
152
147
|
:key="`y-axis-${track.index}-${axis.index}`"
|
|
@@ -160,20 +155,6 @@ watch(
|
|
|
160
155
|
:data-y-axis-side="axis.side"
|
|
161
156
|
:transform="`translate(${axis.x}, 0)`"
|
|
162
157
|
/>
|
|
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
158
|
<g
|
|
178
159
|
v-if="
|
|
179
160
|
!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 () => {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { flushPromises } from '@vue/test-utils'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
3
|
|
|
4
4
|
import { flushAnimationFrames } from '../../test/setup'
|
|
5
|
+
import type { WaveformXAxisLabelFormatter } from '../../types'
|
|
5
6
|
import { type WaveformData } from '../waveform'
|
|
6
7
|
|
|
7
8
|
import { gridSeries, mountSizedChart } from '../../test/waveformChart'
|
|
@@ -162,35 +163,42 @@ describe('WaveformChart', () => {
|
|
|
162
163
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
|
163
164
|
String(trackWidth),
|
|
164
165
|
)
|
|
165
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
166
|
-
expect(wrapper.
|
|
166
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990.3')
|
|
167
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
167
168
|
})
|
|
168
169
|
|
|
169
170
|
it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
|
|
170
171
|
const cases = [
|
|
171
|
-
{ values: [0, 50], exponent: null },
|
|
172
|
-
{ values: [
|
|
173
|
-
{
|
|
172
|
+
{ values: [0, 50], yDomain: [0, 50] as [number, number], exponent: null },
|
|
173
|
+
{ values: [1000, 3000], yDomain: [1000, 3000] as [number, number], exponent: 'E+03' },
|
|
174
|
+
{
|
|
175
|
+
values: [0.0001, 0.0003],
|
|
176
|
+
yDomain: [0.0001, 0.0003] as [number, number],
|
|
177
|
+
exponent: 'E-04',
|
|
178
|
+
},
|
|
174
179
|
]
|
|
175
180
|
|
|
176
|
-
for (const { values, exponent } of cases) {
|
|
177
|
-
const
|
|
178
|
-
kind: 'points',
|
|
181
|
+
for (const { values, yDomain, exponent } of cases) {
|
|
182
|
+
const data = {
|
|
183
|
+
kind: 'points' as const,
|
|
179
184
|
points: values.map((y, x) => ({ x, y })),
|
|
180
|
-
}
|
|
185
|
+
}
|
|
186
|
+
const originalValues = data.points.map((point) => point.y)
|
|
187
|
+
const wrapper = await mountSizedChart(data, { yDomain })
|
|
181
188
|
const labels = wrapper
|
|
182
189
|
.get('.waveform-chart__axis--y')
|
|
183
190
|
.findAll('.tick text')
|
|
184
191
|
.map((tick) => tick.text())
|
|
185
|
-
const
|
|
192
|
+
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
|
186
193
|
|
|
187
194
|
if (exponent === null) {
|
|
188
|
-
expect(
|
|
195
|
+
expect(exponentLabels).toHaveLength(0)
|
|
189
196
|
} else {
|
|
190
|
-
expect(
|
|
191
|
-
expect(
|
|
192
|
-
expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
|
|
197
|
+
expect(exponentLabels).toHaveLength(1)
|
|
198
|
+
expect(exponentLabels[0]).toMatch(new RegExp(`^${exponent.replace('+', '\\+')} `))
|
|
193
199
|
}
|
|
200
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
201
|
+
expect(data.points.map((point) => point.y)).toEqual(originalValues)
|
|
194
202
|
|
|
195
203
|
wrapper.unmount()
|
|
196
204
|
}
|
|
@@ -212,13 +220,13 @@ describe('WaveformChart', () => {
|
|
|
212
220
|
|
|
213
221
|
expect(millisecondsChart.text()).toContain('时间(ms)')
|
|
214
222
|
expect(millisecondTicks.length).toBeGreaterThan(0)
|
|
215
|
-
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
216
|
-
expect(millisecondsChart.
|
|
223
|
+
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
224
|
+
expect(millisecondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
217
225
|
expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
|
|
218
226
|
|
|
219
227
|
const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
|
|
220
228
|
expect(secondsChart.text()).toContain('Elapsed time')
|
|
221
|
-
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1
|
|
229
|
+
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1')
|
|
222
230
|
expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
223
231
|
})
|
|
224
232
|
|
|
@@ -237,15 +245,15 @@ describe('WaveformChart', () => {
|
|
|
237
245
|
|
|
238
246
|
expect(start.attributes('x')).toBe('0')
|
|
239
247
|
expect(start.attributes('text-anchor')).toBe('start')
|
|
240
|
-
expect(start.text()).toBe('0
|
|
248
|
+
expect(start.text()).toBe('0')
|
|
241
249
|
expect(end.attributes('x')).toBe(
|
|
242
250
|
wrapper.get('.waveform-chart__track').attributes('data-track-width'),
|
|
243
251
|
)
|
|
244
252
|
expect(end.attributes('text-anchor')).toBe('end')
|
|
245
|
-
expect(end.text()).toBe('
|
|
253
|
+
expect(end.text()).toBe('1999')
|
|
246
254
|
expect(middleTickLabels.length).toBeGreaterThan(0)
|
|
247
|
-
expect(middleTickLabels).not.toContain('0
|
|
248
|
-
expect(wrapper.
|
|
255
|
+
expect(middleTickLabels).not.toContain('0')
|
|
256
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
249
257
|
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
|
|
250
258
|
middleTickLabels.length,
|
|
251
259
|
)
|
|
@@ -260,6 +268,70 @@ describe('WaveformChart', () => {
|
|
|
260
268
|
expect(endpointGroup.attributes('font-size')).toBe(xAxis.attributes('font-size'))
|
|
261
269
|
})
|
|
262
270
|
|
|
271
|
+
it('formats X-axis ticks and endpoints with display-unit values and source context', async () => {
|
|
272
|
+
const data: WaveformData = {
|
|
273
|
+
kind: 'points',
|
|
274
|
+
points: [
|
|
275
|
+
{ x: 0.125, y: 0 },
|
|
276
|
+
{ x: 1.875, y: 1 },
|
|
277
|
+
],
|
|
278
|
+
}
|
|
279
|
+
const originalPoints = data.points.map((point) => ({ ...point }))
|
|
280
|
+
const labelFormatter: WaveformXAxisLabelFormatter = (value, context) =>
|
|
281
|
+
`${context.kind}:${value / 10}`
|
|
282
|
+
const formatter = vi.fn(labelFormatter)
|
|
283
|
+
const wrapper = await mountSizedChart(data, {
|
|
284
|
+
timeUnit: 'ms',
|
|
285
|
+
axes: { x: { labelFormatter: formatter } },
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('start:12.5')
|
|
289
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('end:187.5')
|
|
290
|
+
expect(
|
|
291
|
+
wrapper
|
|
292
|
+
.get('.waveform-chart__axis--x')
|
|
293
|
+
.findAll('.tick text')
|
|
294
|
+
.every((tick) => tick.text().startsWith('tick:')),
|
|
295
|
+
).toBe(true)
|
|
296
|
+
|
|
297
|
+
const startCall = formatter.mock.calls.find(([, context]) => context.kind === 'start')
|
|
298
|
+
const tickCall = formatter.mock.calls.find(([, context]) => context.kind === 'tick')
|
|
299
|
+
const endCall = formatter.mock.calls.find(([, context]) => context.kind === 'end')
|
|
300
|
+
expect(startCall).toEqual([
|
|
301
|
+
125,
|
|
302
|
+
{
|
|
303
|
+
kind: 'start',
|
|
304
|
+
rawValue: 0.125,
|
|
305
|
+
timeUnit: 'ms',
|
|
306
|
+
domain: [0.125, 1.875],
|
|
307
|
+
displayDomain: [125, 1875],
|
|
308
|
+
},
|
|
309
|
+
])
|
|
310
|
+
expect(tickCall?.[0]).toBeTypeOf('number')
|
|
311
|
+
expect(tickCall?.[1]).toMatchObject({
|
|
312
|
+
kind: 'tick',
|
|
313
|
+
timeUnit: 'ms',
|
|
314
|
+
domain: [0.125, 1.875],
|
|
315
|
+
displayDomain: [125, 1875],
|
|
316
|
+
})
|
|
317
|
+
expect(endCall?.[1]).toMatchObject({ kind: 'end', rawValue: 1.875 })
|
|
318
|
+
expect(data.points).toEqual(originalPoints)
|
|
319
|
+
|
|
320
|
+
await wrapper.setProps({
|
|
321
|
+
axes: { x: { labelFormatter: (value: number) => `updated:${value}` } },
|
|
322
|
+
})
|
|
323
|
+
await flushPromises()
|
|
324
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('updated:125')
|
|
325
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('updated:1875')
|
|
326
|
+
expect(
|
|
327
|
+
wrapper
|
|
328
|
+
.get('.waveform-chart__axis--x')
|
|
329
|
+
.findAll('.tick text')
|
|
330
|
+
.every((tick) => tick.text().startsWith('updated:')),
|
|
331
|
+
).toBe(true)
|
|
332
|
+
expect(data.points).toEqual(originalPoints)
|
|
333
|
+
})
|
|
334
|
+
|
|
263
335
|
it('keeps zoom-change domains in source seconds', async () => {
|
|
264
336
|
const wrapper = await mountSizedChart({
|
|
265
337
|
kind: 'points',
|
|
@@ -300,6 +372,8 @@ describe('WaveformChart', () => {
|
|
|
300
372
|
)
|
|
301
373
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
|
302
374
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
|
303
|
-
expect(
|
|
375
|
+
expect(
|
|
376
|
+
Number.isFinite(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())),
|
|
377
|
+
).toBe(true)
|
|
304
378
|
})
|
|
305
379
|
})
|
|
@@ -59,7 +59,7 @@ describe('WaveformChart', () => {
|
|
|
59
59
|
await flushPromises()
|
|
60
60
|
|
|
61
61
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
|
|
62
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
62
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
63
63
|
})
|
|
64
64
|
|
|
65
65
|
it('updates rendering props and disables zoom interaction', async () => {
|
|
@@ -33,10 +33,10 @@ describe('WaveformChart', () => {
|
|
|
33
33
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
|
34
34
|
)
|
|
35
35
|
|
|
36
|
-
expect(labelX).toBe(-
|
|
36
|
+
expect(labelX).toBe(-74)
|
|
37
37
|
expect(labelBackgroundX).toBe(labelX - 12)
|
|
38
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
39
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
38
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(90)
|
|
39
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(90)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
42
|
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
|
@@ -62,8 +62,8 @@ describe('WaveformChart', () => {
|
|
|
62
62
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
63
63
|
|
|
64
64
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
|
65
|
-
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(
|
|
66
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
65
|
+
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(64)
|
|
66
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
|
|
67
67
|
})
|
|
68
68
|
|
|
69
69
|
it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
|
|
@@ -122,7 +122,7 @@ describe('WaveformChart', () => {
|
|
|
122
122
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
123
123
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
|
124
124
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
|
125
|
-
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(
|
|
125
|
+
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(32)
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
|
|
@@ -262,10 +262,10 @@ describe('WaveformChart', () => {
|
|
|
262
262
|
)
|
|
263
263
|
|
|
264
264
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
|
265
|
-
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-
|
|
266
|
-
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
267
|
-
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-
|
|
268
|
-
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
265
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
|
266
|
+
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
|
267
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
|
268
|
+
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
|
269
269
|
})
|
|
270
270
|
|
|
271
271
|
it('uses an explicit initial x domain override for an independent track', async () => {
|
|
@@ -293,7 +293,7 @@ describe('WaveformChart', () => {
|
|
|
293
293
|
},
|
|
294
294
|
)
|
|
295
295
|
|
|
296
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
296
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
|
297
297
|
})
|
|
298
298
|
|
|
299
299
|
it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
|
|
@@ -312,13 +312,13 @@ describe('WaveformChart', () => {
|
|
|
312
312
|
|
|
313
313
|
await wrapper.setProps({ yDomain: [3, 97] })
|
|
314
314
|
await flushPromises()
|
|
315
|
-
expect(yTickLabels()).toContain('3
|
|
316
|
-
expect(yTickLabels()).toContain('97
|
|
315
|
+
expect(yTickLabels()).toContain('3')
|
|
316
|
+
expect(yTickLabels()).toContain('97')
|
|
317
317
|
|
|
318
318
|
await wrapper.setProps({ yDomain: undefined })
|
|
319
319
|
await flushPromises()
|
|
320
|
-
expect(yTickLabels()).not.toContain('3
|
|
321
|
-
expect(yTickLabels()).not.toContain('97
|
|
320
|
+
expect(yTickLabels()).not.toContain('3')
|
|
321
|
+
expect(yTickLabels()).not.toContain('97')
|
|
322
322
|
})
|
|
323
323
|
|
|
324
324
|
it('keeps annotations bound to their channel while paging', async () => {
|
|
@@ -30,7 +30,7 @@ describe('WaveformChart', () => {
|
|
|
30
30
|
kind: 'points',
|
|
31
31
|
points: [
|
|
32
32
|
{ x: 0, y: 0 },
|
|
33
|
-
{ x: 1, y:
|
|
33
|
+
{ x: 1, y: 2540 },
|
|
34
34
|
],
|
|
35
35
|
},
|
|
36
36
|
},
|
|
@@ -51,13 +51,13 @@ describe('WaveformChart', () => {
|
|
|
51
51
|
)
|
|
52
52
|
|
|
53
53
|
expect(
|
|
54
|
-
wrapper
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
])
|
|
54
|
+
wrapper.findAll('.waveform-chart__axis--y').map((axis) =>
|
|
55
|
+
axis
|
|
56
|
+
.findAll('.tick text')
|
|
57
|
+
.map((label) => label.text())
|
|
58
|
+
.filter((label) => label.startsWith('E')),
|
|
59
|
+
),
|
|
60
|
+
).toEqual([[], [expect.stringMatching(/^E\+03 /)], [expect.stringMatching(/^E-04 /)]])
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
|
@@ -54,8 +54,9 @@ describe('WaveformChart', () => {
|
|
|
54
54
|
],
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
expect(first.
|
|
58
|
-
expect(second.get('.waveform-chart__axis
|
|
57
|
+
expect(first.get('.waveform-chart__axis--y').text()).not.toContain('E')
|
|
58
|
+
expect(second.get('.waveform-chart__axis--y').text()).toContain('E+04 ')
|
|
59
|
+
expect(second.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
59
60
|
})
|
|
60
61
|
|
|
61
62
|
it('renders a configurable zero line only when the Y domain contains zero', async () => {
|
|
@@ -299,8 +300,9 @@ describe('WaveformChart', () => {
|
|
|
299
300
|
})
|
|
300
301
|
expect(xAxis.get('path.domain').attributes('display')).toBe('none')
|
|
301
302
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
|
|
302
|
-
expect(wrapper.
|
|
303
|
-
expect(
|
|
303
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
|
304
|
+
expect(yAxes.some((axis) => axis.text().includes('E+04 '))).toBe(true)
|
|
305
|
+
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
|
304
306
|
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
|
|
305
307
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
|
306
308
|
stroke: '#dc2626',
|
|
@@ -79,8 +79,8 @@ describe('WaveformChart', () => {
|
|
|
79
79
|
|
|
80
80
|
expect(wrapper.emitted('zoom-change')).toBeUndefined()
|
|
81
81
|
expect(wrapper.emitted('zoom-end')).toBeUndefined()
|
|
82
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0
|
|
83
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
82
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
|
83
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
84
84
|
})
|
|
85
85
|
|
|
86
86
|
it('ignores independent viewport dragging', async () => {
|
|
@@ -333,14 +333,14 @@ describe('WaveformChart', () => {
|
|
|
333
333
|
)
|
|
334
334
|
flushAnimationFrames()
|
|
335
335
|
await flushPromises()
|
|
336
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0
|
|
336
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
|
|
337
337
|
|
|
338
338
|
overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
|
|
339
339
|
await flushPromises()
|
|
340
340
|
|
|
341
341
|
expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
|
|
342
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0
|
|
343
|
-
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('
|
|
342
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
|
343
|
+
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
|
344
344
|
})
|
|
345
345
|
|
|
346
346
|
it('exposes resetViewport for independent tracks', async () => {
|
|
@@ -364,13 +364,13 @@ describe('WaveformChart', () => {
|
|
|
364
364
|
)
|
|
365
365
|
flushAnimationFrames()
|
|
366
366
|
await flushPromises()
|
|
367
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0
|
|
367
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
|
|
368
368
|
|
|
369
369
|
const chart = wrapper.vm as unknown as { resetViewport: () => void }
|
|
370
370
|
chart.resetViewport()
|
|
371
371
|
await flushPromises()
|
|
372
372
|
|
|
373
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0
|
|
374
|
-
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('
|
|
373
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
|
|
374
|
+
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
|
375
375
|
})
|
|
376
376
|
})
|
|
@@ -49,15 +49,15 @@ describe('WaveformChart wheel zoom out', () => {
|
|
|
49
49
|
end: wrapper.get('.waveform-chart__axis-endpoint--end').text(),
|
|
50
50
|
})
|
|
51
51
|
|
|
52
|
-
expect(endpoints()).toEqual({ start: '-
|
|
52
|
+
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
|
53
53
|
await dispatchWheel(-4000)
|
|
54
|
-
expect(endpoints()).not.toEqual({ start: '-
|
|
54
|
+
expect(endpoints()).not.toEqual({ start: '-5000', end: '5000' })
|
|
55
55
|
|
|
56
56
|
await wrapper.setProps({ data: createData(-0.125, 0.125) })
|
|
57
57
|
await flushPromises()
|
|
58
58
|
await dispatchWheel(4000)
|
|
59
59
|
|
|
60
|
-
expect(endpoints()).toEqual({ start: '-
|
|
60
|
+
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
it('emits one zoom-end payload after zooming a shared viewport out', async () => {
|
|
@@ -96,6 +96,76 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
|
96
96
|
<Switch v-model:checked="model.yAxisLineVisible" size="small" aria-label="显示纵轴线" />
|
|
97
97
|
</div>
|
|
98
98
|
</div>
|
|
99
|
+
<div class="x-axis-label-controls">
|
|
100
|
+
<label class="frame-style-control frame-style-control--switch">
|
|
101
|
+
<span>自定义 Label</span>
|
|
102
|
+
<Switch
|
|
103
|
+
v-model:checked="model.xAxisLabelFormatterEnabled"
|
|
104
|
+
size="small"
|
|
105
|
+
aria-label="自定义 X 轴 Label"
|
|
106
|
+
/>
|
|
107
|
+
</label>
|
|
108
|
+
<label v-if="model.xAxisLabelFormatterEnabled" class="frame-style-control">
|
|
109
|
+
<span>格式类型</span>
|
|
110
|
+
<Select
|
|
111
|
+
v-model:value="model.xAxisLabelFormat"
|
|
112
|
+
:options="model.xAxisLabelFormatOptions"
|
|
113
|
+
size="small"
|
|
114
|
+
aria-label="X 轴 Label 格式类型"
|
|
115
|
+
/>
|
|
116
|
+
</label>
|
|
117
|
+
<label
|
|
118
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'number'"
|
|
119
|
+
class="frame-style-control"
|
|
120
|
+
>
|
|
121
|
+
<span>运算倍率</span>
|
|
122
|
+
<InputNumber
|
|
123
|
+
v-model:value="model.xAxisLabelMultiplier"
|
|
124
|
+
:min="-1000000"
|
|
125
|
+
:max="1000000"
|
|
126
|
+
:step="0.1"
|
|
127
|
+
size="small"
|
|
128
|
+
aria-label="X 轴 Label 运算倍率"
|
|
129
|
+
/>
|
|
130
|
+
</label>
|
|
131
|
+
<label
|
|
132
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'number'"
|
|
133
|
+
class="frame-style-control"
|
|
134
|
+
>
|
|
135
|
+
<span>小数位数</span>
|
|
136
|
+
<InputNumber
|
|
137
|
+
v-model:value="model.xAxisLabelFractionDigits"
|
|
138
|
+
:min="0"
|
|
139
|
+
:max="12"
|
|
140
|
+
:step="1"
|
|
141
|
+
size="small"
|
|
142
|
+
aria-label="X 轴 Label 小数位数"
|
|
143
|
+
/>
|
|
144
|
+
</label>
|
|
145
|
+
<label
|
|
146
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'datetime'"
|
|
147
|
+
class="frame-style-control"
|
|
148
|
+
>
|
|
149
|
+
<span>时区</span>
|
|
150
|
+
<Select
|
|
151
|
+
v-model:value="model.xAxisLabelTimeZone"
|
|
152
|
+
:options="model.xAxisLabelTimeZoneOptions"
|
|
153
|
+
size="small"
|
|
154
|
+
aria-label="X 轴 Label 时区"
|
|
155
|
+
/>
|
|
156
|
+
</label>
|
|
157
|
+
<label
|
|
158
|
+
v-if="model.xAxisLabelFormatterEnabled && model.xAxisLabelFormat === 'datetime'"
|
|
159
|
+
class="frame-style-control frame-style-control--switch"
|
|
160
|
+
>
|
|
161
|
+
<span>显示毫秒</span>
|
|
162
|
+
<Switch
|
|
163
|
+
v-model:checked="model.xAxisLabelShowMilliseconds"
|
|
164
|
+
size="small"
|
|
165
|
+
aria-label="X 轴 Label 显示毫秒"
|
|
166
|
+
/>
|
|
167
|
+
</label>
|
|
168
|
+
</div>
|
|
99
169
|
</section>
|
|
100
170
|
<section class="control-section">
|
|
101
171
|
<h2>图框样式</h2>
|