waveform-analysis 0.1.73 → 0.1.75
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 +256 -46
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7115
- 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/layout.d.ts +2 -1
- package/dist/types/components/core/types.d.ts +8 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
- package/dist/types/components/core/yAxisTitleLayout.d.ts +1 -1
- package/dist/types/components/interaction/useExtremaTickViewport.d.ts +1 -1
- 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/demo/types.d.ts +2 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/types/types/chart.d.ts +4 -2
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- package/src/App.vue +6 -3
- 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 -4
- 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/useChartImageExport.ts +4 -0
- package/src/components/core/layout.ts +36 -22
- package/src/components/core/trackLayoutBuilder.ts +17 -23
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +24 -20
- package/src/components/core/useWaveformLayout.ts +3 -1
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/waveformChartTypes.ts +2 -1
- package/src/components/core/yAxisLayoutMetrics.ts +24 -16
- package/src/components/core/yAxisPadding.ts +10 -5
- package/src/components/core/yAxisTitleLayout.ts +9 -48
- package/src/components/interaction/useExtremaTickViewport.ts +1 -1
- 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/demo/DemoYAxisPaddingControls.vue +11 -3
- package/src/demo/types.ts +2 -1
- package/src/index.ts +53 -1
- package/src/router.ts +1 -0
- package/src/types/chart.ts +4 -2
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
|
@@ -6,10 +6,11 @@ import type { WaveformAxesOptions } from '../../types'
|
|
|
6
6
|
import { formatXAxisLabel } from '../../utils'
|
|
7
7
|
import { formatYAxisTickLabel } from '../core/layout'
|
|
8
8
|
import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
|
|
9
|
-
import {
|
|
9
|
+
import { scheduleMaximumTickVisibility } from './yAxisTickVisibility'
|
|
10
10
|
import { sameTick } from '../core/yAxisPadding'
|
|
11
11
|
import { resolveYAxisLabelBandWidth, resolveYAxisLabelFontSize } from '../core/yAxisLabelStyle'
|
|
12
12
|
import { formatChannelLabel } from './channelLabel'
|
|
13
|
+
import { updateAxis, updateSvgAttribute } from './updateAxis'
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
15
16
|
track: TrackLayout
|
|
@@ -58,77 +59,89 @@ function renderAxes() {
|
|
|
58
59
|
if (!element) return
|
|
59
60
|
const unit = props.showUnits ? axis.seriesList[0]?.unit : undefined
|
|
60
61
|
const highestTick = Math.max(...axis.tickValues)
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
),
|
|
62
|
+
const lowestTick = Math.min(...axis.tickValues)
|
|
63
|
+
const format = (value: number) =>
|
|
64
|
+
formatYAxisTickLabel(
|
|
65
|
+
value,
|
|
66
|
+
axis.scale.domain() as [number, number],
|
|
67
|
+
axis.tickValues,
|
|
68
|
+
unit,
|
|
69
|
+
axis.side,
|
|
70
70
|
)
|
|
71
|
+
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
|
72
|
+
.tickFormat((value) => format(Number(value)))
|
|
71
73
|
.tickSize(-4)
|
|
72
74
|
.tickPadding(7)
|
|
73
75
|
.tickSizeOuter(0)
|
|
74
76
|
.tickValues(axis.tickValues)
|
|
75
77
|
|
|
76
78
|
const selection = select(element)
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
79
|
+
updateAxis(element, yAxis, axis.scale, axis.tickValues, axis.side, format)
|
|
80
|
+
updateSvgAttribute(element, 'data-label-domain-min', axis.scale.domain()[0])
|
|
81
|
+
updateSvgAttribute(element, 'data-label-domain-max', axis.scale.domain()[1])
|
|
82
|
+
updateSvgAttribute(element, 'data-label-unit', unit ?? null)
|
|
83
|
+
selection.selectAll<SVGGElement, number>('.tick').each(function (value) {
|
|
84
|
+
const text = this.querySelector('text')
|
|
85
|
+
if (text) {
|
|
86
|
+
updateSvgAttribute(text, 'y', null)
|
|
87
|
+
updateSvgAttribute(text, 'dy', '0')
|
|
88
|
+
updateSvgAttribute(text, 'dominant-baseline', 'central')
|
|
89
|
+
updateSvgAttribute(
|
|
90
|
+
text,
|
|
91
|
+
'data-axis-maximum-label',
|
|
92
|
+
props.hideOverlappingMaximumLabel && sameTick(value, highestTick) ? 'true' : null,
|
|
93
|
+
)
|
|
94
|
+
updateSvgAttribute(
|
|
95
|
+
text,
|
|
96
|
+
'data-axis-minimum-label',
|
|
97
|
+
props.hideOverlappingMaximumLabel && sameTick(value, lowestTick) ? 'true' : null,
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
updateSvgAttribute(
|
|
101
|
+
this,
|
|
102
|
+
'data-maximum-tick',
|
|
98
103
|
axis.maximumTick !== undefined && sameTick(value, axis.maximumTick) ? 'true' : null,
|
|
99
104
|
)
|
|
100
|
-
|
|
105
|
+
updateSvgAttribute(
|
|
106
|
+
this,
|
|
107
|
+
'data-minimum-tick',
|
|
101
108
|
axis.minimumTick !== undefined && sameTick(value, axis.minimumTick) ? 'true' : null,
|
|
102
109
|
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
110
|
+
updateSvgAttribute(this, 'data-tick-value', value)
|
|
111
|
+
updateSvgAttribute(this, 'data-tick-y', axis.scale(value))
|
|
112
|
+
})
|
|
113
|
+
selection.selectAll<SVGPathElement, unknown>('path.domain').each(function () {
|
|
114
|
+
updateSvgAttribute(this, 'display', props.axes?.y?.lineVisible === false ? 'none' : null)
|
|
115
|
+
})
|
|
108
116
|
})
|
|
109
117
|
|
|
110
118
|
scheduleVisibility()
|
|
111
119
|
if (!xAxisElement.value) return
|
|
112
120
|
const selection = select(xAxisElement.value)
|
|
113
|
-
|
|
121
|
+
const format = (value: number) =>
|
|
122
|
+
formatXAxisLabel(
|
|
123
|
+
value,
|
|
124
|
+
props.track.xScale.domain() as [number, number],
|
|
125
|
+
props.timeUnit,
|
|
126
|
+
'tick',
|
|
127
|
+
props.axes?.x?.labelFormatter ?? props.track.xAxisLabelFormatter,
|
|
128
|
+
)
|
|
129
|
+
updateAxis(
|
|
130
|
+
xAxisElement.value,
|
|
114
131
|
axisBottom(props.track.xScale)
|
|
115
132
|
.tickValues(props.track.xAxisTickValues)
|
|
116
|
-
.tickFormat((value) =>
|
|
117
|
-
formatXAxisLabel(
|
|
118
|
-
Number(value),
|
|
119
|
-
props.track.xScale.domain() as [number, number],
|
|
120
|
-
props.timeUnit,
|
|
121
|
-
'tick',
|
|
122
|
-
props.axes?.x?.labelFormatter ?? props.track.xAxisLabelFormatter,
|
|
123
|
-
),
|
|
124
|
-
)
|
|
133
|
+
.tickFormat((value) => format(Number(value)))
|
|
125
134
|
.tickSize(-4)
|
|
126
135
|
.tickPadding(7)
|
|
127
136
|
.tickSizeOuter(0),
|
|
137
|
+
props.track.xScale,
|
|
138
|
+
props.track.xAxisTickValues,
|
|
139
|
+
'bottom',
|
|
140
|
+
format,
|
|
128
141
|
)
|
|
129
|
-
selection
|
|
130
|
-
|
|
131
|
-
|
|
142
|
+
selection.selectAll<SVGPathElement, unknown>('path.domain').each(function () {
|
|
143
|
+
updateSvgAttribute(this, 'display', props.axes?.x?.lineVisible === false ? 'none' : null)
|
|
144
|
+
})
|
|
132
145
|
}
|
|
133
146
|
|
|
134
147
|
let disposed = false
|
|
@@ -136,7 +149,7 @@ function scheduleVisibility() {
|
|
|
136
149
|
if (disposed) return
|
|
137
150
|
const apply = () => {
|
|
138
151
|
const svg = yAxisElements.value[0]?.ownerSVGElement
|
|
139
|
-
if (svg)
|
|
152
|
+
if (svg) scheduleMaximumTickVisibility(svg)
|
|
140
153
|
}
|
|
141
154
|
apply()
|
|
142
155
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import { ref, watchEffect } from 'vue'
|
|
3
|
+
import { updateGrid } from './updateGrid'
|
|
2
4
|
import type { TrackLayout } from '../core/types'
|
|
3
5
|
|
|
4
6
|
interface Props {
|
|
@@ -13,7 +15,35 @@ interface Props {
|
|
|
13
15
|
}
|
|
14
16
|
}
|
|
15
17
|
|
|
16
|
-
defineProps<Props>()
|
|
18
|
+
const props = defineProps<Props>()
|
|
19
|
+
const minorVertical = ref<SVGGElement>()
|
|
20
|
+
const minorHorizontal = ref<SVGGElement>()
|
|
21
|
+
const majorVertical = ref<SVGGElement>()
|
|
22
|
+
const majorHorizontal = ref<SVGGElement>()
|
|
23
|
+
watchEffect(
|
|
24
|
+
() => {
|
|
25
|
+
const track = props.track
|
|
26
|
+
for (const [element, direction, ticks] of [
|
|
27
|
+
[minorVertical.value, 'vertical', track.xMinorTicks],
|
|
28
|
+
[minorHorizontal.value, 'horizontal', track.yMinorTicks],
|
|
29
|
+
[majorVertical.value, 'vertical', track.xMajorTicks],
|
|
30
|
+
[majorHorizontal.value, 'horizontal', track.yMajorTicks],
|
|
31
|
+
] as const) {
|
|
32
|
+
if (element)
|
|
33
|
+
updateGrid(
|
|
34
|
+
element,
|
|
35
|
+
track.gridLines[direction] ? ticks : [],
|
|
36
|
+
direction === 'vertical' ? track.xScale : track.yScale,
|
|
37
|
+
direction,
|
|
38
|
+
direction === 'vertical' ? track.height : (track.width ?? props.innerWidth),
|
|
39
|
+
direction === 'vertical'
|
|
40
|
+
? track.gridLines.verticalColor
|
|
41
|
+
: track.gridLines.horizontalColor,
|
|
42
|
+
)
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
{ flush: 'post' },
|
|
46
|
+
)
|
|
17
47
|
</script>
|
|
18
48
|
|
|
19
49
|
<template>
|
|
@@ -34,58 +64,14 @@ defineProps<Props>()
|
|
|
34
64
|
<g
|
|
35
65
|
class="waveform-track__grid waveform-track__grid--minor waveform-chart__grid waveform-chart__grid--minor"
|
|
36
66
|
>
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
v-for="tick in track.xMinorTicks"
|
|
40
|
-
:key="`x-minor-${track.index}-${tick}`"
|
|
41
|
-
data-grid-direction="vertical"
|
|
42
|
-
:stroke="track.gridLines.verticalColor"
|
|
43
|
-
:x1="track.xScale(tick)"
|
|
44
|
-
:x2="track.xScale(tick)"
|
|
45
|
-
y1="0"
|
|
46
|
-
:y2="track.height"
|
|
47
|
-
/>
|
|
48
|
-
</template>
|
|
49
|
-
<template v-if="track.gridLines.horizontal">
|
|
50
|
-
<line
|
|
51
|
-
v-for="tick in track.yMinorTicks"
|
|
52
|
-
:key="`y-minor-${track.index}-${tick}`"
|
|
53
|
-
data-grid-direction="horizontal"
|
|
54
|
-
:stroke="track.gridLines.horizontalColor"
|
|
55
|
-
x1="0"
|
|
56
|
-
:x2="track.width ?? innerWidth"
|
|
57
|
-
:y1="track.yScale(tick)"
|
|
58
|
-
:y2="track.yScale(tick)"
|
|
59
|
-
/>
|
|
60
|
-
</template>
|
|
67
|
+
<g ref="minorVertical" />
|
|
68
|
+
<g ref="minorHorizontal" />
|
|
61
69
|
</g>
|
|
62
70
|
<g
|
|
63
71
|
class="waveform-track__grid waveform-track__grid--major waveform-chart__grid waveform-chart__grid--major"
|
|
64
72
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
v-for="tick in track.xMajorTicks"
|
|
68
|
-
:key="`x-major-${track.index}-${tick}`"
|
|
69
|
-
data-grid-direction="vertical"
|
|
70
|
-
:stroke="track.gridLines.verticalColor"
|
|
71
|
-
:x1="track.xScale(tick)"
|
|
72
|
-
:x2="track.xScale(tick)"
|
|
73
|
-
y1="0"
|
|
74
|
-
:y2="track.height"
|
|
75
|
-
/>
|
|
76
|
-
</template>
|
|
77
|
-
<template v-if="track.gridLines.horizontal">
|
|
78
|
-
<line
|
|
79
|
-
v-for="tick in track.yMajorTicks"
|
|
80
|
-
:key="`y-major-${track.index}-${tick}`"
|
|
81
|
-
data-grid-direction="horizontal"
|
|
82
|
-
:stroke="track.gridLines.horizontalColor"
|
|
83
|
-
x1="0"
|
|
84
|
-
:x2="track.width ?? innerWidth"
|
|
85
|
-
:y1="track.yScale(tick)"
|
|
86
|
-
:y2="track.yScale(tick)"
|
|
87
|
-
/>
|
|
88
|
-
</template>
|
|
73
|
+
<g ref="majorVertical" />
|
|
74
|
+
<g ref="majorHorizontal" />
|
|
89
75
|
</g>
|
|
90
76
|
</g>
|
|
91
77
|
|
|
@@ -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
|
+
}
|
|
@@ -24,9 +24,17 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
|
|
|
24
24
|
:aria-label="side === 'upper' ? '上方留白比例' : '下方留白比例'"
|
|
25
25
|
/>
|
|
26
26
|
</div>
|
|
27
|
-
<div
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
<div
|
|
28
|
+
v-for="side in ['maximum', 'minimum'] as const"
|
|
29
|
+
:key="side"
|
|
30
|
+
class="y-axis-padding-control extrema-control"
|
|
31
|
+
>
|
|
32
|
+
<span>{{ side === 'maximum' ? '显示最大值刻度' : '显示最小值刻度' }}</span>
|
|
33
|
+
<Switch
|
|
34
|
+
v-model:checked="model[`${side}TickVisible`]"
|
|
35
|
+
size="small"
|
|
36
|
+
:aria-label="side === 'maximum' ? '显示最大值刻度' : '显示最小值刻度'"
|
|
37
|
+
/>
|
|
30
38
|
</div>
|
|
31
39
|
<small>仅作用于自动 Y 范围;刻度取整可能增加实际留白。</small>
|
|
32
40
|
</section>
|
package/src/demo/types.ts
CHANGED
|
@@ -53,7 +53,8 @@ export interface DemoControlPanelModel {
|
|
|
53
53
|
yAxisLineVisible: boolean
|
|
54
54
|
upperPaddingEnabled: boolean
|
|
55
55
|
lowerPaddingEnabled: boolean
|
|
56
|
-
|
|
56
|
+
maximumTickVisible: boolean
|
|
57
|
+
minimumTickVisible: boolean
|
|
57
58
|
upperPaddingPercent: number
|
|
58
59
|
lowerPaddingPercent: number
|
|
59
60
|
xAxisLabelFormatterEnabled: boolean
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,58 @@
|
|
|
7
7
|
export { default as WaveformChart } from './components/WaveformChart.vue'
|
|
8
8
|
export type { WaveformChartProps } from './components/core/waveformChartTypes'
|
|
9
9
|
|
|
10
|
+
// Numeric engine, pure preset factories and owner-scoped chart animation.
|
|
11
|
+
export { getEasingFunction } from './animation/core/easing'
|
|
12
|
+
export { WaveformAnimationController } from './animation/WaveformAnimationController'
|
|
13
|
+
export {
|
|
14
|
+
createLineDrawingAnimation,
|
|
15
|
+
createStaggeredLineDrawingAnimations,
|
|
16
|
+
} from './animation/presets/lineDrawing'
|
|
17
|
+
export {
|
|
18
|
+
createPathTransitionAnimation,
|
|
19
|
+
createYAxisScaleAnimation,
|
|
20
|
+
} from './animation/presets/transitions'
|
|
21
|
+
export {
|
|
22
|
+
createAnnotationAppearAnimation,
|
|
23
|
+
createAnnotationDisappearAnimation,
|
|
24
|
+
} from './animation/presets/annotation'
|
|
25
|
+
export { Animation } from './animation/core/Animation'
|
|
26
|
+
export { Animator, globalAnimator } from './animation/core/Animator'
|
|
27
|
+
export type {
|
|
28
|
+
AnimationValue,
|
|
29
|
+
AnimationDirection,
|
|
30
|
+
AnimationState,
|
|
31
|
+
AnimationFrame,
|
|
32
|
+
AnimationTrack,
|
|
33
|
+
AnimationConfig,
|
|
34
|
+
AnimationHandle,
|
|
35
|
+
AnimationScheduler,
|
|
36
|
+
EasingFunction,
|
|
37
|
+
EasingFamily,
|
|
38
|
+
EasingType,
|
|
39
|
+
} from './animation/core/types'
|
|
40
|
+
export type {
|
|
41
|
+
LineDrawingDirection,
|
|
42
|
+
AnnotationAnimationType,
|
|
43
|
+
LineDrawingOptions,
|
|
44
|
+
StaggeredLineDrawingOptions,
|
|
45
|
+
DataUpdateOptions,
|
|
46
|
+
AnnotationAnimationOptions,
|
|
47
|
+
WaveformEnterAnimationOptions,
|
|
48
|
+
WaveformUpdateAnimationOptions,
|
|
49
|
+
WaveformAnnotationAnimationOptions,
|
|
50
|
+
WaveformAnimationProps,
|
|
51
|
+
ClipAnimationState,
|
|
52
|
+
AnimationBinding,
|
|
53
|
+
NumericGeometry,
|
|
54
|
+
GeometryTransitionInput,
|
|
55
|
+
GeometryTransitionFrame,
|
|
56
|
+
AnnotationAnimationState,
|
|
57
|
+
AnimationScope,
|
|
58
|
+
AnimationObjectKey,
|
|
59
|
+
WaveformAnimationControllerOptions,
|
|
60
|
+
} from './animation/types'
|
|
61
|
+
|
|
10
62
|
// 类型定义
|
|
11
63
|
export type {
|
|
12
64
|
// 图表类型
|
|
@@ -40,7 +92,7 @@ export type {
|
|
|
40
92
|
WaveformXAxisLabelKind,
|
|
41
93
|
WaveformXAxisLabelFormatterContext,
|
|
42
94
|
WaveformXAxisLabelFormatter,
|
|
43
|
-
// Includes axes.y.
|
|
95
|
+
// Includes axes.y.maximumTickVisible and minimumTickVisible for independent extrema ticks.
|
|
44
96
|
WaveformAxesOptions,
|
|
45
97
|
WaveformYAxisLabelTextStyle,
|
|
46
98
|
WaveformZeroLineOptions,
|
package/src/router.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { createRouter, createWebHashHistory, type RouteRecordRaw } from 'vue-rou
|
|
|
3
3
|
import App from './App.vue'
|
|
4
4
|
|
|
5
5
|
export const routes: RouteRecordRaw[] = [
|
|
6
|
+
{ path: '/animation', name: 'animation', component: () => import('./views/AnimationDemo.vue') },
|
|
6
7
|
{
|
|
7
8
|
path: '/',
|
|
8
9
|
name: 'workspace',
|
package/src/types/chart.ts
CHANGED
|
@@ -289,8 +289,10 @@ export interface WaveformAxesOptions {
|
|
|
289
289
|
splitNumber?: number
|
|
290
290
|
/** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
|
|
291
291
|
nice?: boolean
|
|
292
|
-
/** Shows
|
|
293
|
-
|
|
292
|
+
/** Shows the full data maximum's extra tick and label. Defaults to true. */
|
|
293
|
+
maximumTickVisible?: boolean
|
|
294
|
+
/** Shows the full data minimum's extra tick and label. Defaults to true. */
|
|
295
|
+
minimumTickVisible?: boolean
|
|
294
296
|
/** Fraction of the automatic Y span added above its maximum. Defaults to 0. */
|
|
295
297
|
upperPaddingRatio?: number
|
|
296
298
|
/** Fraction of the automatic Y span added below its minimum. Defaults to 0. */
|