waveform-analysis 0.1.74 → 0.1.76
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 +422 -60
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7119
- package/dist/style.css +1 -1
- package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
- package/dist/types/animation/core/Animation.d.ts +13 -0
- package/dist/types/animation/core/Animator.d.ts +23 -0
- package/dist/types/animation/core/easing.d.ts +3 -0
- package/dist/types/animation/core/errors.d.ts +4 -0
- package/dist/types/animation/core/handle.d.ts +7 -0
- package/dist/types/animation/core/owner.d.ts +3 -0
- package/dist/types/animation/core/propertyGroups.d.ts +17 -0
- package/dist/types/animation/core/task.d.ts +29 -0
- package/dist/types/animation/core/types.d.ts +74 -0
- package/dist/types/animation/core/validation.d.ts +30 -0
- package/dist/types/animation/core/values.d.ts +8 -0
- package/dist/types/animation/presets/annotation.d.ts +4 -0
- package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
- package/dist/types/animation/presets/shared.d.ts +14 -0
- package/dist/types/animation/presets/transitions.d.ts +4 -0
- package/dist/types/animation/types.d.ts +84 -0
- package/dist/types/components/WaveformChart.vue.d.ts +3 -2
- package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
- package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
- package/dist/types/components/animation/config.d.ts +31 -0
- package/dist/types/components/animation/context.d.ts +20 -0
- package/dist/types/components/animation/geometry.d.ts +10 -0
- package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
- package/dist/types/components/animation/pathBuffer.d.ts +4 -0
- package/dist/types/components/animation/trackPresentation.d.ts +35 -0
- package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
- package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
- package/dist/types/components/annotation/types.d.ts +7 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
- package/dist/types/components/core/amplitudeLimits.d.ts +6 -0
- package/dist/types/components/core/layout.d.ts +6 -9
- package/dist/types/components/core/trackLayoutBuilder.d.ts +3 -1
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +26 -1
- package/dist/types/components/core/useWaveformData.d.ts +2 -2
- package/dist/types/components/core/useWaveformMeasurements.d.ts +4 -1
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +6 -3
- package/dist/types/components/core/xAxisTicks.d.ts +2 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisTicks.d.ts +6 -0
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/interaction/integerZoom.d.ts +3 -3
- package/dist/types/components/interaction/stepZoom.d.ts +5 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +0 -3
- package/dist/types/components/interaction/useWaveformZoom.d.ts +0 -2
- package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
- package/dist/types/components/rendering/updateAxis.d.ts +5 -0
- package/dist/types/components/rendering/updateGrid.d.ts +3 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
- package/dist/types/components/utils/assignElement.d.ts +2 -0
- package/dist/types/core/data.d.ts +1 -1
- package/dist/types/demo/DemoChartHost.vue.d.ts +1 -1
- package/dist/types/demo/remoteWaveformLoader.d.ts +23 -0
- package/dist/types/demo/types.d.ts +11 -2
- package/dist/types/demo/useRemoteWaveformWindow.d.ts +22 -0
- package/dist/types/index.d.ts +10 -1
- package/dist/types/types/chart.d.ts +8 -1
- package/dist/types/types/index.d.ts +1 -1
- package/dist/types/utils/formatters.d.ts +4 -4
- package/dist/types/utils/xAxisNumber.d.ts +2 -0
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/App.vue +34 -100
- package/src/DemoRouterApp.vue +1 -0
- package/src/animation/WaveformAnimationController.ts +328 -0
- package/src/animation/core/Animation.ts +37 -0
- package/src/animation/core/Animator.ts +200 -0
- package/src/animation/core/easing.ts +74 -0
- package/src/animation/core/errors.ts +24 -0
- package/src/animation/core/handle.ts +34 -0
- package/src/animation/core/owner.ts +9 -0
- package/src/animation/core/propertyGroups.ts +143 -0
- package/src/animation/core/task.ts +153 -0
- package/src/animation/core/types.ts +81 -0
- package/src/animation/core/validation.ts +173 -0
- package/src/animation/core/values.ts +38 -0
- package/src/animation/presets/annotation.ts +66 -0
- package/src/animation/presets/lineDrawing.ts +51 -0
- package/src/animation/presets/shared.ts +58 -0
- package/src/animation/presets/transitions.ts +77 -0
- package/src/animation/types.ts +81 -0
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/WaveformChartView.vue +13 -6
- package/src/components/animation/annotationPresentation.ts +145 -0
- package/src/components/animation/chartExportVersion.ts +5 -0
- package/src/components/animation/config.ts +87 -0
- package/src/components/animation/context.ts +56 -0
- package/src/components/animation/geometry.ts +148 -0
- package/src/components/animation/paginationPresentation.ts +114 -0
- package/src/components/animation/pathBuffer.ts +61 -0
- package/src/components/animation/trackPresentation.ts +172 -0
- package/src/components/animation/useWaveformAnimations.ts +420 -0
- package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
- package/src/components/annotation/types.ts +3 -0
- package/src/components/controls/useChartCommands.ts +12 -13
- package/src/components/controls/useChartImageExport.ts +4 -0
- package/src/components/core/amplitudeLimits.ts +44 -0
- package/src/components/core/layout.ts +22 -34
- package/src/components/core/trackLayoutBuilder.ts +21 -23
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +27 -23
- package/src/components/core/useWaveformChartLifecycle.ts +11 -2
- package/src/components/core/useWaveformData.ts +11 -5
- package/src/components/core/useWaveformLayout.ts +9 -4
- package/src/components/core/useWaveformMeasurements.ts +3 -0
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/useWaveformYAxisLayout.ts +2 -0
- package/src/components/core/waveformChartTypes.ts +6 -2
- package/src/components/core/xAxisTicks.ts +69 -0
- package/src/components/core/yAxisLayoutMetrics.ts +4 -0
- package/src/components/core/yAxisTicks.ts +39 -0
- package/src/components/data/types.ts +2 -0
- package/src/components/interaction/integerZoom.ts +31 -12
- package/src/components/interaction/remoteViewport.ts +26 -4
- package/src/components/interaction/stepZoom.ts +33 -0
- package/src/components/interaction/useWaveformViewport.ts +3 -26
- package/src/components/interaction/useWaveformZoom.ts +50 -157
- package/src/components/interaction/zoomConstraints.ts +5 -5
- package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
- package/src/components/rendering/WaveformTrack.vue +6 -1
- package/src/components/rendering/WaveformTrackAxes.vue +64 -51
- package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
- package/src/components/rendering/updateAxis.ts +57 -0
- package/src/components/rendering/updateGrid.ts +42 -0
- package/src/components/rendering/yAxisTickVisibility.ts +45 -18
- package/src/components/utils/assignElement.ts +7 -0
- package/src/core/data.ts +5 -2
- package/src/data/simulatedWaveforms.ts +0 -10
- package/src/demo/DemoChartHost.vue +49 -4
- package/src/demo/remoteWaveformLoader.ts +114 -0
- package/src/demo/types.ts +11 -1
- package/src/demo/useRemoteWaveformWindow.ts +229 -0
- package/src/index.ts +54 -0
- package/src/router.ts +1 -0
- package/src/types/chart.ts +10 -1
- package/src/types/index.ts +2 -0
- package/src/utils/formatters.ts +10 -5
- package/src/utils/xAxisNumber.ts +36 -0
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
- package/dist/types/data/sx2Channel.d.ts +0 -10
- package/src/data/sx2Channel.ts +0 -315
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { select } from 'd3'
|
|
2
|
+
import type { Axis, NumberValue, ScaleLinear } from 'd3'
|
|
3
|
+
|
|
4
|
+
const previous = new WeakMap<
|
|
5
|
+
SVGGElement,
|
|
6
|
+
{ ticks: number[]; range: number[]; orientation: string }
|
|
7
|
+
>()
|
|
8
|
+
|
|
9
|
+
/** Avoid invalidating native SVG layout when an attribute already has the required value. */
|
|
10
|
+
export function updateSvgAttribute(element: Element, name: string, value: string | number | null) {
|
|
11
|
+
const next = value === null ? null : String(value)
|
|
12
|
+
if (element.getAttribute(name) === next) return
|
|
13
|
+
if (next === null) element.removeAttribute(name)
|
|
14
|
+
else element.setAttribute(name, next)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Domain animation moves existing ticks; unchanged labels keep their native text nodes. */
|
|
18
|
+
export function updateAxis(
|
|
19
|
+
element: SVGGElement,
|
|
20
|
+
axis: Axis<NumberValue>,
|
|
21
|
+
scale: ScaleLinear<number, number>,
|
|
22
|
+
ticks: number[],
|
|
23
|
+
orientation: 'bottom' | 'left' | 'right',
|
|
24
|
+
format: (value: number) => string,
|
|
25
|
+
) {
|
|
26
|
+
const range = scale.range()
|
|
27
|
+
const cached = previous.get(element)
|
|
28
|
+
const selection = select(element)
|
|
29
|
+
if (
|
|
30
|
+
!cached ||
|
|
31
|
+
cached.ticks !== ticks ||
|
|
32
|
+
cached.orientation !== orientation ||
|
|
33
|
+
range.some((value, index) => value !== cached.range[index])
|
|
34
|
+
) {
|
|
35
|
+
selection.call(axis)
|
|
36
|
+
previous.set(element, { ticks, range, orientation })
|
|
37
|
+
} else {
|
|
38
|
+
const horizontal = orientation === 'bottom'
|
|
39
|
+
const offset = axis.offset()
|
|
40
|
+
selection
|
|
41
|
+
.selectAll<SVGGElement, number>('.tick')
|
|
42
|
+
.each(function (value) {
|
|
43
|
+
updateSvgAttribute(
|
|
44
|
+
this,
|
|
45
|
+
'transform',
|
|
46
|
+
horizontal
|
|
47
|
+
? `translate(${scale(value) + offset},0)`
|
|
48
|
+
: `translate(0,${scale(value) + offset})`,
|
|
49
|
+
)
|
|
50
|
+
})
|
|
51
|
+
.select<SVGTextElement>('text')
|
|
52
|
+
.each(function (value) {
|
|
53
|
+
const label = format(value)
|
|
54
|
+
if (this.textContent !== label) this.textContent = label
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { updateSvgAttribute } from './updateAxis'
|
|
2
|
+
import type { ScaleLinear } from 'd3'
|
|
3
|
+
|
|
4
|
+
const nodes = new WeakMap<SVGGElement, Map<number, SVGLineElement>>()
|
|
5
|
+
|
|
6
|
+
/** Keep each grid line independently discoverable by the axis collision pass. */
|
|
7
|
+
export function updateGrid(
|
|
8
|
+
group: SVGGElement,
|
|
9
|
+
ticks: readonly number[],
|
|
10
|
+
scale: ScaleLinear<number, number>,
|
|
11
|
+
direction: 'vertical' | 'horizontal',
|
|
12
|
+
extent: number,
|
|
13
|
+
color: string | undefined,
|
|
14
|
+
) {
|
|
15
|
+
let lines = nodes.get(group)
|
|
16
|
+
if (!lines) nodes.set(group, (lines = new Map()))
|
|
17
|
+
const current = new Set(ticks)
|
|
18
|
+
for (const [tick, line] of lines) {
|
|
19
|
+
if (!current.has(tick)) {
|
|
20
|
+
line.remove()
|
|
21
|
+
lines.delete(tick)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
let previous: SVGLineElement | undefined
|
|
25
|
+
for (const tick of ticks) {
|
|
26
|
+
let line = lines.get(tick)
|
|
27
|
+
if (!line) {
|
|
28
|
+
line = document.createElementNS('http://www.w3.org/2000/svg', 'line')
|
|
29
|
+
line.setAttribute('data-grid-direction', direction)
|
|
30
|
+
lines.set(tick, line)
|
|
31
|
+
}
|
|
32
|
+
const next = previous ? previous.nextSibling : group.firstChild
|
|
33
|
+
if (next !== line) group.insertBefore(line, next)
|
|
34
|
+
const position = scale(tick)
|
|
35
|
+
updateSvgAttribute(line, 'stroke', color ?? null)
|
|
36
|
+
updateSvgAttribute(line, 'x1', direction === 'vertical' ? position : 0)
|
|
37
|
+
updateSvgAttribute(line, 'x2', direction === 'vertical' ? position : extent)
|
|
38
|
+
updateSvgAttribute(line, 'y1', direction === 'horizontal' ? position : 0)
|
|
39
|
+
updateSvgAttribute(line, 'y2', direction === 'horizontal' ? position : extent)
|
|
40
|
+
previous = line
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sameTick } from '../core/yAxisPadding'
|
|
2
2
|
import { formatYAxisTickLabel } from '../core/layout'
|
|
3
|
+
import { updateSvgAttribute } from './updateAxis'
|
|
3
4
|
|
|
4
5
|
interface Box {
|
|
5
6
|
left: number
|
|
@@ -42,14 +43,25 @@ export function linesOverlap(a: number, b: number, widthA = 1, widthB = 1): bool
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
function hide(element: Element) {
|
|
45
|
-
element
|
|
46
|
-
element
|
|
46
|
+
updateSvgAttribute(element, 'display', 'none')
|
|
47
|
+
updateSvgAttribute(element, 'data-maximum-hidden', 'true')
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
function strokeWidth(element: Element): number {
|
|
50
51
|
return Number.parseFloat(getComputedStyle(element).strokeWidth) || 1
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
const pending = new WeakSet<SVGSVGElement>()
|
|
55
|
+
/** Coalesce sibling axes after Vue's current DOM patch, without adding a late animation frame. */
|
|
56
|
+
export function scheduleMaximumTickVisibility(svg: SVGSVGElement) {
|
|
57
|
+
if (pending.has(svg)) return
|
|
58
|
+
pending.add(svg)
|
|
59
|
+
queueMicrotask(() => {
|
|
60
|
+
pending.delete(svg)
|
|
61
|
+
updateMaximumTickVisibility(svg)
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
function formatCompactLabels(svg: SVGSVGElement) {
|
|
54
66
|
svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
|
|
55
67
|
const axis = maximum.parentElement!.parentElement!
|
|
@@ -64,13 +76,14 @@ function formatCompactLabels(svg: SVGSVGElement) {
|
|
|
64
76
|
Number(axis.getAttribute('data-label-domain-max')),
|
|
65
77
|
]
|
|
66
78
|
labels.forEach((text) => {
|
|
67
|
-
|
|
79
|
+
const label = formatYAxisTickLabel(
|
|
68
80
|
value(text),
|
|
69
81
|
domain,
|
|
70
82
|
visibleValues,
|
|
71
83
|
axis.getAttribute('data-label-unit') ?? undefined,
|
|
72
84
|
axis.getAttribute('data-y-axis-side') === 'right' ? 'right' : 'left',
|
|
73
85
|
)
|
|
86
|
+
if (text.textContent !== label) text.textContent = label
|
|
74
87
|
})
|
|
75
88
|
})
|
|
76
89
|
}
|
|
@@ -78,24 +91,38 @@ function formatCompactLabels(svg: SVGSVGElement) {
|
|
|
78
91
|
/** Apply after axes and grid are in the DOM; never feed the filtered set back into layout. */
|
|
79
92
|
export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
80
93
|
svg.querySelectorAll('[data-maximum-hidden]').forEach((el) => {
|
|
81
|
-
el
|
|
82
|
-
el
|
|
94
|
+
updateSvgAttribute(el, 'display', null)
|
|
95
|
+
updateSvgAttribute(el, 'data-maximum-hidden', null)
|
|
83
96
|
})
|
|
84
97
|
svg.querySelectorAll('.waveform-track__axis--y .tick text').forEach((text) => {
|
|
85
|
-
text
|
|
98
|
+
updateSvgAttribute(text, 'display', null)
|
|
86
99
|
})
|
|
87
100
|
formatCompactLabels(svg)
|
|
101
|
+
const boxes = new Map<SVGTextElement, Box>()
|
|
102
|
+
const widths = new Map<Element, number>()
|
|
103
|
+
const box = (text: SVGTextElement) => {
|
|
104
|
+
if (!boxes.has(text)) boxes.set(text, textBox(text))
|
|
105
|
+
return boxes.get(text)!
|
|
106
|
+
}
|
|
107
|
+
const width = (element: Element) => {
|
|
108
|
+
if (!widths.has(element)) widths.set(element, strokeWidth(element))
|
|
109
|
+
return widths.get(element)!
|
|
110
|
+
}
|
|
88
111
|
const minimumLabels = [...svg.querySelectorAll<SVGTextElement>('text[data-axis-minimum-label]')]
|
|
89
112
|
svg.querySelectorAll<SVGTextElement>('text[data-axis-maximum-label]').forEach((maximum) => {
|
|
90
|
-
const
|
|
91
|
-
if (minimumLabels.some((minimum) => labelsOverlap(
|
|
113
|
+
const maximumBox = box(maximum)
|
|
114
|
+
if (minimumLabels.some((minimum) => labelsOverlap(maximumBox, box(minimum)))) hide(maximum)
|
|
92
115
|
})
|
|
93
116
|
svg.querySelectorAll<SVGGElement>('.waveform-track__axis--y .tick').forEach((tick) => {
|
|
94
117
|
const position = Number(tick.getAttribute('data-tick-y'))
|
|
95
118
|
const height = Number(tick.closest('.waveform-track')?.getAttribute('data-track-height'))
|
|
96
119
|
const line = tick.querySelector('line')
|
|
97
|
-
if (
|
|
98
|
-
|
|
120
|
+
if (line)
|
|
121
|
+
updateSvgAttribute(
|
|
122
|
+
line,
|
|
123
|
+
'display',
|
|
124
|
+
sameTick(position, 0) || sameTick(position, height) ? 'none' : null,
|
|
125
|
+
)
|
|
99
126
|
})
|
|
100
127
|
const minimumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-minimum-tick]')]
|
|
101
128
|
const maximumTicks = [...svg.querySelectorAll<SVGGElement>('.tick[data-maximum-tick]')]
|
|
@@ -105,7 +132,7 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
|
105
132
|
if (!text) return
|
|
106
133
|
maximumTicks.forEach((maximum) => {
|
|
107
134
|
const other = maximum.querySelector('text')
|
|
108
|
-
if (other && other !== text && labelsOverlap(
|
|
135
|
+
if (other && other !== text && labelsOverlap(box(text), box(other))) hide(other)
|
|
109
136
|
})
|
|
110
137
|
})
|
|
111
138
|
const extremeTicks = [...new Set([...minimumTicks, ...maximumTicks])]
|
|
@@ -113,14 +140,14 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
|
113
140
|
const text = tick.querySelector('text')
|
|
114
141
|
const line = tick.querySelector('line')
|
|
115
142
|
if (!text || !line) return
|
|
116
|
-
const
|
|
143
|
+
const textBounds = box(text)
|
|
117
144
|
labels.forEach((other) => {
|
|
118
145
|
if (text.getAttribute('display') === 'none') return
|
|
119
146
|
if (other.getAttribute('display') === 'none') return
|
|
120
147
|
if (other.parentElement?.matches('[data-minimum-tick], [data-maximum-tick]')) return
|
|
121
148
|
if (!tick.hasAttribute('data-minimum-tick') && other.hasAttribute('data-axis-minimum-label'))
|
|
122
149
|
return
|
|
123
|
-
if (labelsOverlap(
|
|
150
|
+
if (labelsOverlap(textBounds, box(other))) hide(other)
|
|
124
151
|
})
|
|
125
152
|
const axis = tick.parentElement!
|
|
126
153
|
const value = Number(tick.getAttribute('data-tick-value'))
|
|
@@ -134,8 +161,8 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
|
134
161
|
linesOverlap(
|
|
135
162
|
position,
|
|
136
163
|
Number(other.getAttribute('data-tick-y')),
|
|
137
|
-
|
|
138
|
-
|
|
164
|
+
width(line),
|
|
165
|
+
width(otherLine),
|
|
139
166
|
)
|
|
140
167
|
)
|
|
141
168
|
hide(otherLine)
|
|
@@ -145,12 +172,12 @@ export function updateMaximumTickVisibility(svg: SVGSVGElement) {
|
|
|
145
172
|
const track = axis.closest('.waveform-track')!
|
|
146
173
|
track.querySelectorAll('[data-grid-direction="horizontal"]').forEach((gridLine) => {
|
|
147
174
|
const gridY = Number(gridLine.getAttribute('y1'))
|
|
148
|
-
if (linesOverlap(position, gridY,
|
|
175
|
+
if (linesOverlap(position, gridY, width(line), width(gridLine))) hide(gridLine)
|
|
149
176
|
})
|
|
150
177
|
if (tick.hasAttribute('data-maximum-tick'))
|
|
151
|
-
tick
|
|
178
|
+
updateSvgAttribute(tick, 'data-maximum-value', value)
|
|
152
179
|
if (tick.hasAttribute('data-minimum-tick'))
|
|
153
|
-
tick
|
|
180
|
+
updateSvgAttribute(tick, 'data-minimum-value', value)
|
|
154
181
|
})
|
|
155
182
|
formatCompactLabels(svg)
|
|
156
183
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ComponentPublicInstance, Ref } from 'vue'
|
|
2
|
+
export function assignElement<T extends Element>(
|
|
3
|
+
target: Ref<T | undefined>,
|
|
4
|
+
element: Element | ComponentPublicInstance | null,
|
|
5
|
+
) {
|
|
6
|
+
target.value = element instanceof Element ? (element as T) : undefined
|
|
7
|
+
}
|
package/src/core/data.ts
CHANGED
|
@@ -215,10 +215,11 @@ function normalizedSeriesOptions(
|
|
|
215
215
|
/** Internal normalized series which preserves compact TypedArray storage for chart rendering. */
|
|
216
216
|
export function normalizeWaveformSeriesSources(
|
|
217
217
|
data: WaveformData,
|
|
218
|
+
retainEmpty = false,
|
|
218
219
|
): NormalizedWaveformSourceSeries[] {
|
|
219
220
|
if (data.kind !== 'series') {
|
|
220
221
|
const source = createWaveformPointSource(data, normalizeWaveformData)
|
|
221
|
-
return source.length
|
|
222
|
+
return source.length || retainEmpty
|
|
222
223
|
? [
|
|
223
224
|
{
|
|
224
225
|
...normalizedSeriesOptions('series-0', { name: '' }),
|
|
@@ -239,7 +240,9 @@ export function normalizeWaveformSeriesSources(
|
|
|
239
240
|
}
|
|
240
241
|
usedIds.add(uniqueId)
|
|
241
242
|
const source = createWaveformPointSource(series.data, normalizeWaveformData)
|
|
242
|
-
return source.length
|
|
243
|
+
return source.length || retainEmpty
|
|
244
|
+
? [{ ...normalizedSeriesOptions(uniqueId, series), source }]
|
|
245
|
+
: []
|
|
243
246
|
})
|
|
244
247
|
}
|
|
245
248
|
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { WaveformData, WaveformPoint, WaveformSeries } from '../types'
|
|
2
2
|
import { recordedDampedPoints } from './recordedDampedWaveform'
|
|
3
|
-
import { sx2Channel } from './sx2Channel'
|
|
4
3
|
|
|
5
4
|
const POINT_COUNT = 1_000
|
|
6
5
|
const START_TIME = -5
|
|
@@ -177,15 +176,6 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|
|
177
176
|
pointType: 'none',
|
|
178
177
|
signal: (time) => 6e-9 * Math.exp(-(time + 5) * 0.2) * Math.cos(TWO_PI * 0.8 * time),
|
|
179
178
|
},
|
|
180
|
-
{
|
|
181
|
-
id: `recorded-${sx2Channel.chnl_id}`,
|
|
182
|
-
shotNo: String(sx2Channel.shot),
|
|
183
|
-
name: sx2Channel.chnl,
|
|
184
|
-
unit: sx2Channel.dat_unit,
|
|
185
|
-
lineType: 'linear',
|
|
186
|
-
pointType: 'none',
|
|
187
|
-
signal: sx2Channel.data.map((value, index) => [sx2Channel.time[index]!, value]),
|
|
188
|
-
},
|
|
189
179
|
]
|
|
190
180
|
|
|
191
181
|
export function createSimulatedWaveformData(trackCount?: number): WaveformData {
|
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { ref } from 'vue'
|
|
2
|
+
import { computed, ref } from 'vue'
|
|
3
3
|
|
|
4
4
|
import { WaveformChart } from '../components'
|
|
5
5
|
import type { DemoChartModel } from './types'
|
|
6
|
+
import { applyXDomainStrategy } from '../components/core/xDomain'
|
|
6
7
|
|
|
7
8
|
const model = defineModel<DemoChartModel>('model', { required: true })
|
|
9
|
+
const xDomainStrategy = { type: 'integer-ms' as const, includeExplicit: true }
|
|
10
|
+
const panXDomain = computed(() =>
|
|
11
|
+
applyXDomainStrategy(model.value.recordDomain, xDomainStrategy, true),
|
|
12
|
+
)
|
|
8
13
|
|
|
9
14
|
const waveformChartRef = ref<{ resetViewport: (trackIndex?: number) => void }>()
|
|
10
15
|
|
|
11
|
-
function resetViewport() {
|
|
12
|
-
waveformChartRef.value?.resetViewport()
|
|
16
|
+
function resetViewport(trackIndex?: number) {
|
|
17
|
+
waveformChartRef.value?.resetViewport(trackIndex)
|
|
13
18
|
}
|
|
14
19
|
|
|
15
20
|
defineExpose({ resetViewport })
|
|
@@ -17,6 +22,17 @@ defineExpose({ resetViewport })
|
|
|
17
22
|
|
|
18
23
|
<template>
|
|
19
24
|
<section class="chart-panel">
|
|
25
|
+
<div
|
|
26
|
+
v-if="model.loading || model.errors.length"
|
|
27
|
+
class="remote-window-status"
|
|
28
|
+
aria-live="polite"
|
|
29
|
+
>
|
|
30
|
+
<span v-if="model.loading" role="status">正在加载当前时间范围…</span>
|
|
31
|
+
<template v-if="model.errors.length">
|
|
32
|
+
<span role="alert">{{ model.errors.join(';') }}</span>
|
|
33
|
+
<button type="button" :disabled="model.loading" @click="model.retry()">重试</button>
|
|
34
|
+
</template>
|
|
35
|
+
</div>
|
|
20
36
|
<WaveformChart
|
|
21
37
|
ref="waveformChartRef"
|
|
22
38
|
layout-preset="edge-compact"
|
|
@@ -25,15 +41,19 @@ defineExpose({ resetViewport })
|
|
|
25
41
|
v-model:hidden-series-ids="model.hiddenSeriesIds"
|
|
26
42
|
:data="model.data"
|
|
27
43
|
:min-visible-points="2"
|
|
44
|
+
:min-zoom-span="model.minZoomSpan"
|
|
45
|
+
:max-zoom-scale="null"
|
|
46
|
+
:pan-x-domain="panXDomain"
|
|
28
47
|
:integer-zoom="model.integerZoom"
|
|
29
48
|
:initial-x-domain="model.initialXDomain"
|
|
30
|
-
:x-domain-strategy="
|
|
49
|
+
:x-domain-strategy="xDomainStrategy"
|
|
31
50
|
:display-mode="model.displayMode"
|
|
32
51
|
:overlay-mode="model.overlayMode"
|
|
33
52
|
:grid="{
|
|
34
53
|
rowCount: model.rowCount,
|
|
35
54
|
columnCount: model.columnCount,
|
|
36
55
|
showPagination: true,
|
|
56
|
+
fillIncompleteLastRow: true,
|
|
37
57
|
trackLines: model.gridTrackLines,
|
|
38
58
|
}"
|
|
39
59
|
:title="model.title"
|
|
@@ -55,8 +75,33 @@ defineExpose({ resetViewport })
|
|
|
55
75
|
v-model:interaction-mode="model.interactionMode"
|
|
56
76
|
:toolbar="model.toolbarVisible"
|
|
57
77
|
pannable
|
|
78
|
+
@zoom-intent="model.handleZoomIntent"
|
|
79
|
+
@zoom-change="model.handleZoomChange"
|
|
58
80
|
@zoom-end="model.handleZoomEnd"
|
|
81
|
+
@pan-end="model.handlePanEnd"
|
|
59
82
|
@zoom-reset="model.resetWaveformViewport"
|
|
60
83
|
/>
|
|
61
84
|
</section>
|
|
62
85
|
</template>
|
|
86
|
+
|
|
87
|
+
<style scoped>
|
|
88
|
+
.chart-panel {
|
|
89
|
+
position: relative;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.remote-window-status {
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 6px;
|
|
95
|
+
right: 12px;
|
|
96
|
+
z-index: 2;
|
|
97
|
+
display: flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
gap: 8px;
|
|
100
|
+
max-width: calc(100% - 24px);
|
|
101
|
+
padding: 6px 10px;
|
|
102
|
+
border: 1px solid #d0d5dd;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
background: #fff;
|
|
105
|
+
color: #344054;
|
|
106
|
+
}
|
|
107
|
+
</style>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { InjectionKey } from 'vue'
|
|
2
|
+
import { normalizeWaveformSeries } from '../core'
|
|
3
|
+
import { normalizeWaveformSeriesSources } from '../core/data'
|
|
4
|
+
import type { WaveformData } from '../types'
|
|
5
|
+
|
|
6
|
+
export interface RemoteWindowRequest {
|
|
7
|
+
start: number
|
|
8
|
+
end: number
|
|
9
|
+
seriesIds?: string[]
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type RemoteWindowLoader = (
|
|
13
|
+
request: RemoteWindowRequest,
|
|
14
|
+
signal: AbortSignal,
|
|
15
|
+
) => Promise<WaveformData>
|
|
16
|
+
|
|
17
|
+
export const remoteWindowLoaderKey: InjectionKey<RemoteWindowLoader> = Symbol('remoteWindowLoader')
|
|
18
|
+
|
|
19
|
+
export interface MockWindowOptions {
|
|
20
|
+
delayMs?: number | ((request: RemoteWindowRequest) => number)
|
|
21
|
+
empty?: boolean | ((request: RemoteWindowRequest) => boolean)
|
|
22
|
+
fail?: boolean | ((request: RemoteWindowRequest) => boolean)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Preserve series identities even when a window has no samples. */
|
|
26
|
+
export function selectWaveformWindow(
|
|
27
|
+
data: WaveformData,
|
|
28
|
+
request: RemoteWindowRequest,
|
|
29
|
+
empty = false,
|
|
30
|
+
): WaveformData {
|
|
31
|
+
const series = normalizeWaveformSeriesSources(data, true)
|
|
32
|
+
const selected = request.seriesIds ? new Set(request.seriesIds) : undefined
|
|
33
|
+
const windows = series
|
|
34
|
+
.filter((item) => !selected || selected.has(item.id))
|
|
35
|
+
.map(({ source, ...item }) => {
|
|
36
|
+
const range = source.visibleRange([request.start, request.end])
|
|
37
|
+
return {
|
|
38
|
+
...item,
|
|
39
|
+
data: {
|
|
40
|
+
kind: 'points' as const,
|
|
41
|
+
points: empty ? [] : source.pointsInRange(range.start, range.end),
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
})
|
|
45
|
+
return data.kind === 'series'
|
|
46
|
+
? { kind: 'series', series: windows }
|
|
47
|
+
: (windows[0]?.data ?? { kind: 'points', points: [] })
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function mergeWaveformWindow(
|
|
51
|
+
current: WaveformData,
|
|
52
|
+
response: WaveformData,
|
|
53
|
+
seriesIds?: string[],
|
|
54
|
+
): WaveformData {
|
|
55
|
+
if (!seriesIds || current.kind !== 'series' || response.kind !== 'series') return response
|
|
56
|
+
const selected = new Set(seriesIds)
|
|
57
|
+
const byId = new Map(response.series.map((series) => [series.id, series]))
|
|
58
|
+
return {
|
|
59
|
+
kind: 'series',
|
|
60
|
+
series: current.series.map((series) =>
|
|
61
|
+
series.id && selected.has(series.id) ? (byId.get(series.id) ?? series) : series,
|
|
62
|
+
),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Metadata is derived from the complete record, never from a loaded window. */
|
|
67
|
+
export function waveformRecordMetadata(data: WaveformData) {
|
|
68
|
+
let minimum = Infinity
|
|
69
|
+
let maximum = -Infinity
|
|
70
|
+
let minZoomSpan = Infinity
|
|
71
|
+
for (const series of normalizeWaveformSeries(data)) {
|
|
72
|
+
for (let index = 0; index < series.points.length; index++) {
|
|
73
|
+
const x = series.points[index].x
|
|
74
|
+
minimum = Math.min(minimum, x)
|
|
75
|
+
maximum = Math.max(maximum, x)
|
|
76
|
+
const previous = series.points[index - 1]?.x
|
|
77
|
+
if (previous !== undefined && x > previous) {
|
|
78
|
+
minZoomSpan = Math.min(minZoomSpan, x - previous)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
domain: (minimum < maximum ? [minimum, maximum] : [0, 1]) as [number, number],
|
|
84
|
+
minZoomSpan: Number.isFinite(minZoomSpan) ? minZoomSpan : undefined,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function createMockWindowLoader(
|
|
89
|
+
source: () => WaveformData,
|
|
90
|
+
options: MockWindowOptions = {},
|
|
91
|
+
): RemoteWindowLoader {
|
|
92
|
+
return (request, signal) =>
|
|
93
|
+
new Promise((resolve, reject) => {
|
|
94
|
+
const abort = () => {
|
|
95
|
+
clearTimeout(timer)
|
|
96
|
+
signal.removeEventListener('abort', abort)
|
|
97
|
+
reject(new DOMException('Request cancelled', 'AbortError'))
|
|
98
|
+
}
|
|
99
|
+
const delay =
|
|
100
|
+
typeof options.delayMs === 'function' ? options.delayMs(request) : options.delayMs
|
|
101
|
+
const timer = setTimeout(() => {
|
|
102
|
+
signal.removeEventListener('abort', abort)
|
|
103
|
+
const fail = typeof options.fail === 'function' ? options.fail(request) : options.fail
|
|
104
|
+
if (fail) {
|
|
105
|
+
reject(new Error('模拟数据加载失败'))
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
const empty = typeof options.empty === 'function' ? options.empty(request) : options.empty
|
|
109
|
+
resolve(selectWaveformWindow(source(), request, empty))
|
|
110
|
+
}, delay ?? 80)
|
|
111
|
+
signal.addEventListener('abort', abort, { once: true })
|
|
112
|
+
if (signal.aborted) abort()
|
|
113
|
+
})
|
|
114
|
+
}
|
package/src/demo/types.ts
CHANGED
|
@@ -14,7 +14,9 @@ import type {
|
|
|
14
14
|
WaveformTitleOptions,
|
|
15
15
|
WaveformZeroLineOptions,
|
|
16
16
|
WaveformZoomEndPayload,
|
|
17
|
+
WaveformZoomIntentPayload,
|
|
17
18
|
} from '../components'
|
|
19
|
+
import type { WaveformZoomResetPayload, WaveformPanEndPayload } from '../types'
|
|
18
20
|
import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls'
|
|
19
21
|
|
|
20
22
|
interface SelectOption<T> {
|
|
@@ -97,6 +99,11 @@ export interface DemoChartModel {
|
|
|
97
99
|
toolbarVisible: boolean
|
|
98
100
|
data: WaveformData
|
|
99
101
|
initialXDomain?: [number, number]
|
|
102
|
+
recordDomain: [number, number]
|
|
103
|
+
minZoomSpan?: number
|
|
104
|
+
loading: boolean
|
|
105
|
+
errors: string[]
|
|
106
|
+
retry: () => Promise<void[]>
|
|
100
107
|
displayMode: WaveformDisplayMode
|
|
101
108
|
overlayMode: WaveformOverlayMode
|
|
102
109
|
rowCount: number
|
|
@@ -120,5 +127,8 @@ export interface DemoChartModel {
|
|
|
120
127
|
interactionMode: WaveformInteractionMode
|
|
121
128
|
hiddenSeriesIds: string[]
|
|
122
129
|
handleZoomEnd: (payload: WaveformZoomEndPayload) => void | Promise<void>
|
|
123
|
-
|
|
130
|
+
handleZoomIntent: (payload: WaveformZoomIntentPayload) => void
|
|
131
|
+
handleZoomChange: (domain: [number, number]) => void
|
|
132
|
+
handlePanEnd: (payload: WaveformPanEndPayload) => void | Promise<void>
|
|
133
|
+
resetWaveformViewport: (payload?: WaveformZoomResetPayload) => void | Promise<void>
|
|
124
134
|
}
|