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.
Files changed (112) hide show
  1. package/README.md +256 -46
  2. package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
  3. package/dist/index.cjs +1 -2
  4. package/dist/index.js +1 -7115
  5. package/dist/style.css +1 -1
  6. package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
  7. package/dist/types/animation/core/Animation.d.ts +13 -0
  8. package/dist/types/animation/core/Animator.d.ts +23 -0
  9. package/dist/types/animation/core/easing.d.ts +3 -0
  10. package/dist/types/animation/core/errors.d.ts +4 -0
  11. package/dist/types/animation/core/handle.d.ts +7 -0
  12. package/dist/types/animation/core/owner.d.ts +3 -0
  13. package/dist/types/animation/core/propertyGroups.d.ts +17 -0
  14. package/dist/types/animation/core/task.d.ts +29 -0
  15. package/dist/types/animation/core/types.d.ts +74 -0
  16. package/dist/types/animation/core/validation.d.ts +30 -0
  17. package/dist/types/animation/core/values.d.ts +8 -0
  18. package/dist/types/animation/presets/annotation.d.ts +4 -0
  19. package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
  20. package/dist/types/animation/presets/shared.d.ts +14 -0
  21. package/dist/types/animation/presets/transitions.d.ts +4 -0
  22. package/dist/types/animation/types.d.ts +84 -0
  23. package/dist/types/components/WaveformChart.vue.d.ts +3 -2
  24. package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
  25. package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
  26. package/dist/types/components/animation/config.d.ts +31 -0
  27. package/dist/types/components/animation/context.d.ts +20 -0
  28. package/dist/types/components/animation/geometry.d.ts +10 -0
  29. package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
  30. package/dist/types/components/animation/pathBuffer.d.ts +4 -0
  31. package/dist/types/components/animation/trackPresentation.d.ts +35 -0
  32. package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
  33. package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
  34. package/dist/types/components/annotation/types.d.ts +7 -0
  35. package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
  36. package/dist/types/components/core/layout.d.ts +2 -1
  37. package/dist/types/components/core/types.d.ts +8 -0
  38. package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
  39. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  40. package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
  41. package/dist/types/components/core/yAxisTitleLayout.d.ts +1 -1
  42. package/dist/types/components/interaction/useExtremaTickViewport.d.ts +1 -1
  43. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  44. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  45. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  46. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  47. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  48. package/dist/types/components/utils/assignElement.d.ts +2 -0
  49. package/dist/types/demo/types.d.ts +2 -1
  50. package/dist/types/index.d.ts +9 -0
  51. package/dist/types/types/chart.d.ts +4 -2
  52. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  53. package/package.json +5 -2
  54. package/src/App.vue +6 -3
  55. package/src/DemoRouterApp.vue +1 -0
  56. package/src/animation/WaveformAnimationController.ts +328 -0
  57. package/src/animation/core/Animation.ts +37 -0
  58. package/src/animation/core/Animator.ts +200 -0
  59. package/src/animation/core/easing.ts +74 -0
  60. package/src/animation/core/errors.ts +24 -0
  61. package/src/animation/core/handle.ts +34 -0
  62. package/src/animation/core/owner.ts +9 -0
  63. package/src/animation/core/propertyGroups.ts +143 -0
  64. package/src/animation/core/task.ts +153 -0
  65. package/src/animation/core/types.ts +81 -0
  66. package/src/animation/core/validation.ts +173 -0
  67. package/src/animation/core/values.ts +38 -0
  68. package/src/animation/presets/annotation.ts +66 -0
  69. package/src/animation/presets/lineDrawing.ts +51 -0
  70. package/src/animation/presets/shared.ts +58 -0
  71. package/src/animation/presets/transitions.ts +77 -0
  72. package/src/animation/types.ts +81 -0
  73. package/src/components/WaveformChart.vue +1 -0
  74. package/src/components/WaveformChartView.vue +13 -4
  75. package/src/components/animation/annotationPresentation.ts +145 -0
  76. package/src/components/animation/chartExportVersion.ts +5 -0
  77. package/src/components/animation/config.ts +87 -0
  78. package/src/components/animation/context.ts +56 -0
  79. package/src/components/animation/geometry.ts +148 -0
  80. package/src/components/animation/paginationPresentation.ts +114 -0
  81. package/src/components/animation/pathBuffer.ts +61 -0
  82. package/src/components/animation/trackPresentation.ts +172 -0
  83. package/src/components/animation/useWaveformAnimations.ts +420 -0
  84. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  85. package/src/components/annotation/types.ts +3 -0
  86. package/src/components/controls/useChartImageExport.ts +4 -0
  87. package/src/components/core/layout.ts +36 -22
  88. package/src/components/core/trackLayoutBuilder.ts +17 -23
  89. package/src/components/core/types.ts +4 -0
  90. package/src/components/core/useWaveformChartController.ts +24 -20
  91. package/src/components/core/useWaveformLayout.ts +3 -1
  92. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  93. package/src/components/core/waveformChartTypes.ts +2 -1
  94. package/src/components/core/yAxisLayoutMetrics.ts +24 -16
  95. package/src/components/core/yAxisPadding.ts +10 -5
  96. package/src/components/core/yAxisTitleLayout.ts +9 -48
  97. package/src/components/interaction/useExtremaTickViewport.ts +1 -1
  98. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  99. package/src/components/rendering/WaveformTrack.vue +6 -1
  100. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  101. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  102. package/src/components/rendering/updateAxis.ts +57 -0
  103. package/src/components/rendering/updateGrid.ts +42 -0
  104. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  105. package/src/components/utils/assignElement.ts +7 -0
  106. package/src/demo/DemoYAxisPaddingControls.vue +11 -3
  107. package/src/demo/types.ts +2 -1
  108. package/src/index.ts +53 -1
  109. package/src/router.ts +1 -0
  110. package/src/types/chart.ts +4 -2
  111. package/src/views/AnimationDemo.vue +129 -0
  112. 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 { updateMaximumTickVisibility } from './yAxisTickVisibility'
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 yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
62
- .tickFormat((value) =>
63
- formatYAxisTickLabel(
64
- Number(value),
65
- axis.scale.domain() as [number, number],
66
- axis.tickValues,
67
- unit,
68
- axis.side,
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
- selection.call(yAxis)
78
- selection
79
- .selectAll<SVGTextElement, number>('.tick text')
80
- .attr('y', null)
81
- .attr('dy', '0')
82
- .attr('dominant-baseline', 'central')
83
- .attr('data-axis-maximum-label', (value) =>
84
- props.hideOverlappingMaximumLabel && sameTick(value, highestTick) ? 'true' : null,
85
- )
86
- .attr('data-axis-minimum-label', (value) =>
87
- props.hideOverlappingMaximumLabel && sameTick(value, Math.min(...axis.tickValues))
88
- ? 'true'
89
- : null,
90
- )
91
- selection
92
- .attr('data-label-domain-min', axis.scale.domain()[0])
93
- .attr('data-label-domain-max', axis.scale.domain()[1])
94
- .attr('data-label-unit', unit ?? null)
95
- selection
96
- .selectAll<SVGGElement, number>('.tick')
97
- .attr('data-maximum-tick', (value) =>
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
- .attr('data-minimum-tick', (value) =>
105
+ updateSvgAttribute(
106
+ this,
107
+ 'data-minimum-tick',
101
108
  axis.minimumTick !== undefined && sameTick(value, axis.minimumTick) ? 'true' : null,
102
109
  )
103
- .attr('data-tick-value', (value) => value)
104
- .attr('data-tick-y', (value) => axis.scale(value))
105
- selection
106
- .selectAll('path.domain')
107
- .attr('display', props.axes?.y?.lineVisible === false ? 'none' : null)
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
- selection.call(
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
- .selectAll('path.domain')
131
- .attr('display', props.axes?.x?.lineVisible === false ? 'none' : null)
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) updateMaximumTickVisibility(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
- <template v-if="track.gridLines.vertical">
38
- <line
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
- <template v-if="track.gridLines.vertical">
66
- <line
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.setAttribute('display', 'none')
46
- element.setAttribute('data-maximum-hidden', 'true')
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
- text.textContent = formatYAxisTickLabel(
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.removeAttribute('display')
82
- el.removeAttribute('data-maximum-hidden')
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.removeAttribute('display')
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 box = textBox(maximum)
91
- if (minimumLabels.some((minimum) => labelsOverlap(box, textBox(minimum)))) hide(maximum)
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 (sameTick(position, 0) || sameTick(position, height)) line?.setAttribute('display', 'none')
98
- else line?.removeAttribute('display')
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(textBox(text), textBox(other))) hide(other)
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 box = textBox(text)
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(box, textBox(other))) hide(other)
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
- strokeWidth(line),
138
- strokeWidth(otherLine),
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, strokeWidth(line), strokeWidth(gridLine))) hide(gridLine)
175
+ if (linesOverlap(position, gridY, width(line), width(gridLine))) hide(gridLine)
149
176
  })
150
177
  if (tick.hasAttribute('data-maximum-tick'))
151
- tick.setAttribute('data-maximum-value', String(value))
178
+ updateSvgAttribute(tick, 'data-maximum-value', value)
152
179
  if (tick.hasAttribute('data-minimum-tick'))
153
- tick.setAttribute('data-minimum-value', String(value))
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 class="y-axis-padding-control extrema-control">
28
- <span>显示最值刻度</span>
29
- <Switch v-model:checked="model.extremaTicksVisible" size="small" aria-label="显示最值刻度" />
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
- extremaTicksVisible: boolean
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.extremaTicksVisible for independent extrema tick visibility.
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',
@@ -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 extra ticks for the full data extrema, independently of padding. Defaults to true. */
293
- extremaTicksVisible?: boolean
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. */