waveform-analysis 0.1.74 → 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 (97) hide show
  1. package/README.md +246 -39
  2. package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
  3. package/dist/index.cjs +1 -2
  4. package/dist/index.js +1 -7119
  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/types.d.ts +8 -0
  37. package/dist/types/components/core/useWaveformChartController.d.ts +25 -0
  38. package/dist/types/components/core/useWaveformRenderSampling.d.ts +2 -0
  39. package/dist/types/components/core/waveformChartTypes.d.ts +2 -1
  40. package/dist/types/components/rendering/WaveformSeriesLayer.vue.d.ts +1 -0
  41. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +4 -3
  42. package/dist/types/components/rendering/updateAxis.d.ts +5 -0
  43. package/dist/types/components/rendering/updateGrid.d.ts +3 -0
  44. package/dist/types/components/rendering/yAxisTickVisibility.d.ts +2 -0
  45. package/dist/types/components/utils/assignElement.d.ts +2 -0
  46. package/dist/types/index.d.ts +9 -0
  47. package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
  48. package/package.json +5 -2
  49. package/src/DemoRouterApp.vue +1 -0
  50. package/src/animation/WaveformAnimationController.ts +328 -0
  51. package/src/animation/core/Animation.ts +37 -0
  52. package/src/animation/core/Animator.ts +200 -0
  53. package/src/animation/core/easing.ts +74 -0
  54. package/src/animation/core/errors.ts +24 -0
  55. package/src/animation/core/handle.ts +34 -0
  56. package/src/animation/core/owner.ts +9 -0
  57. package/src/animation/core/propertyGroups.ts +143 -0
  58. package/src/animation/core/task.ts +153 -0
  59. package/src/animation/core/types.ts +81 -0
  60. package/src/animation/core/validation.ts +173 -0
  61. package/src/animation/core/values.ts +38 -0
  62. package/src/animation/presets/annotation.ts +66 -0
  63. package/src/animation/presets/lineDrawing.ts +51 -0
  64. package/src/animation/presets/shared.ts +58 -0
  65. package/src/animation/presets/transitions.ts +77 -0
  66. package/src/animation/types.ts +81 -0
  67. package/src/components/WaveformChart.vue +1 -0
  68. package/src/components/WaveformChartView.vue +13 -4
  69. package/src/components/animation/annotationPresentation.ts +145 -0
  70. package/src/components/animation/chartExportVersion.ts +5 -0
  71. package/src/components/animation/config.ts +87 -0
  72. package/src/components/animation/context.ts +56 -0
  73. package/src/components/animation/geometry.ts +148 -0
  74. package/src/components/animation/paginationPresentation.ts +114 -0
  75. package/src/components/animation/pathBuffer.ts +61 -0
  76. package/src/components/animation/trackPresentation.ts +172 -0
  77. package/src/components/animation/useWaveformAnimations.ts +420 -0
  78. package/src/components/annotation/WaveformAnnotationLayer.vue +43 -30
  79. package/src/components/annotation/types.ts +3 -0
  80. package/src/components/controls/useChartImageExport.ts +4 -0
  81. package/src/components/core/trackLayoutBuilder.ts +9 -18
  82. package/src/components/core/types.ts +4 -0
  83. package/src/components/core/useWaveformChartController.ts +24 -20
  84. package/src/components/core/useWaveformRenderSampling.ts +23 -1
  85. package/src/components/core/waveformChartTypes.ts +2 -1
  86. package/src/components/rendering/WaveformSeriesLayer.vue +13 -1
  87. package/src/components/rendering/WaveformTrack.vue +6 -1
  88. package/src/components/rendering/WaveformTrackAxes.vue +64 -51
  89. package/src/components/rendering/WaveformTrackBackdrop.vue +35 -49
  90. package/src/components/rendering/updateAxis.ts +57 -0
  91. package/src/components/rendering/updateGrid.ts +42 -0
  92. package/src/components/rendering/yAxisTickVisibility.ts +45 -18
  93. package/src/components/utils/assignElement.ts +7 -0
  94. package/src/index.ts +52 -0
  95. package/src/router.ts +1 -0
  96. package/src/views/AnimationDemo.vue +129 -0
  97. package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
@@ -1,4 +1,6 @@
1
- import { chartExportOptions } from '../controls/chartExportOptions'
1
+ import { assignElement } from '../utils/assignElement'
2
+ import { chartExportVersion } from '../animation/chartExportVersion'
3
+ import { useWaveformAnimations } from '../animation/useWaveformAnimations'
2
4
  import { useChartImageExport } from '../controls/useChartImageExport'
3
5
  import { useControlMode } from '../controls/useControlMode'
4
6
  import { useChartCommands } from '../controls/useChartCommands'
@@ -16,9 +18,7 @@ import {
16
18
  watch,
17
19
  watchEffect,
18
20
  type ComponentPublicInstance,
19
- type Ref,
20
21
  } from 'vue'
21
-
22
22
  import { useWaveformInstanceId } from '../../utils/waveformId'
23
23
  import { useWaveformAnnotationInteraction, type AnnotationSeriesCandidate } from '../annotation'
24
24
  import { useWaveformChartAnnotations } from '../annotation/useWaveformChartAnnotations'
@@ -41,14 +41,6 @@ import type {
41
41
  ViewportSelectionState,
42
42
  WaveformChartEmit,
43
43
  } from './waveformChartTypes'
44
-
45
- function assignElement<T extends Element>(
46
- target: Ref<T | undefined>,
47
- element: Element | ComponentPublicInstance | null,
48
- ) {
49
- target.value = element instanceof Element ? (element as T) : undefined
50
- }
51
-
52
44
  export function useWaveformChartController(
53
45
  inputProps: ResolvedWaveformChartProps,
54
46
  emit: WaveformChartEmit,
@@ -186,7 +178,17 @@ export function useWaveformChartController(
186
178
  renderingOptions,
187
179
  linePointOverrides,
188
180
  })
189
-
181
+ const animations = useWaveformAnimations({
182
+ props,
183
+ currentPage,
184
+ containerSize: computed(() => [observedWidth.value, observedHeight.value]),
185
+ layoutReady: computed(() => observedWidth.value > 0 && observedHeight.value > 0),
186
+ tracks: trackLayouts,
187
+ preparedSeries,
188
+ innerWidth,
189
+ innerHeight: presentation.innerHeight,
190
+ sampling,
191
+ })
190
192
  const paginationVisible = computed(
191
193
  () => gridOptions.value.showPagination && pageCount.value > 1 && !isCleanView.value,
192
194
  )
@@ -389,10 +391,8 @@ export function useWaveformChartController(
389
391
  height: chartHeight.value,
390
392
  titleHeight: titleAreaHeight.value,
391
393
  }),
392
- version: () => [
393
- props.data,
394
- JSON.stringify({
395
- ...chartExportOptions(props),
394
+ version: () =>
395
+ chartExportVersion(props, {
396
396
  hidden: [...hiddenSeriesIdSet.value],
397
397
  width: chartWidth.value,
398
398
  height: chartHeight.value,
@@ -404,7 +404,8 @@ export function useWaveformChartController(
404
404
  independentY: independentYDomains.value,
405
405
  boundaries: boundaries.value,
406
406
  }),
407
- ],
407
+ prepare: animations.prepareExport,
408
+ release: animations.releaseExport,
408
409
  ready: sampling.ready,
409
410
  })
410
411
 
@@ -418,7 +419,11 @@ export function useWaveformChartController(
418
419
  ...annotations,
419
420
  ...viewport,
420
421
  ...hover,
421
- ...commands,
422
+ ...animations.wrap(commands),
423
+ presentationTracks: animations.presentationTracks,
424
+ presentationAnnotations: animations.presentationAnnotations,
425
+ paginationOpacity: animations.paginationOpacity,
426
+ stabilizeAnimations: animations.stabilizeAnimations,
422
427
  ...imageExport,
423
428
  margin,
424
429
  getPageSize,
@@ -431,7 +436,7 @@ export function useWaveformChartController(
431
436
  contextMenu: annotationInteraction.contextMenu,
432
437
  editorSeriesOptions,
433
438
  clipPathId,
434
- goToPage: lifecycle.goToPage,
439
+ goToPage: animations.wrapPagination(lifecycle.goToPage, pageCount),
435
440
  setContainer: (element: Element | ComponentPublicInstance | null) =>
436
441
  assignElement(container, element),
437
442
  setSvgElement: (element: Element | ComponentPublicInstance | null) =>
@@ -442,5 +447,4 @@ export function useWaveformChartController(
442
447
  assignElement(sharedOverlayElement, element),
443
448
  })
444
449
  }
445
-
446
450
  export type WaveformChartController = ReturnType<typeof useWaveformChartController>
@@ -103,6 +103,7 @@ function errorPayload(
103
103
  }
104
104
 
105
105
  export function useWaveformRenderSampling(context: SamplingContext) {
106
+ const resultState = shallowRef<'success' | 'fallback' | 'error'>('success')
106
107
  const settledSignature = shallowRef<string>()
107
108
  const dataEpoch = shallowRef(0)
108
109
  const session = createWaveformSamplingSession()
@@ -292,10 +293,14 @@ export function useWaveformRenderSampling(context: SamplingContext) {
292
293
  requestId: ++session.requestId,
293
294
  series: batch,
294
295
  })
295
- if (result.type !== 'sample-viewport-response') return
296
+ if (result.type !== 'sample-viewport-response') {
297
+ if (samplingScheduler.isCurrent(token)) resultState.value = 'error'
298
+ return
299
+ }
296
300
  response = result
297
301
  } catch (error) {
298
302
  if (!samplingScheduler.isCurrent(token)) return
303
+ resultState.value = 'error'
299
304
  emitErrorOnce(
300
305
  errorPayload(
301
306
  error instanceof Error ? error.message : String(error),
@@ -310,8 +315,18 @@ export function useWaveformRenderSampling(context: SamplingContext) {
310
315
  return
311
316
  }
312
317
  if (!samplingScheduler.isCurrent(token)) return
318
+ if (response.workerError || client.workerFailureReason) resultState.value = 'fallback'
313
319
  const sourceById = new Map(sampledTargets.map((target) => [target.series.id, target.source]))
320
+ if (
321
+ sampledTargets.some(
322
+ (target) =>
323
+ !response.results.some((result) => result.seriesId === target.series.id && result.output),
324
+ )
325
+ )
326
+ resultState.value = 'error'
314
327
  response.results.forEach((result) => {
328
+ if (result.status !== 'ok' && resultState.value !== 'error')
329
+ resultState.value = result.output ? 'fallback' : 'error'
315
330
  const source = sourceById.get(result.seriesId)
316
331
  const selected = source ? sourcePoints(result, source) : undefined
317
332
  if (selected && (result.status === 'ok' || result.status === 'wasm-unavailable')) {
@@ -369,6 +384,7 @@ export function useWaveformRenderSampling(context: SamplingContext) {
369
384
  const scheduleSampling = (hysteresis: boolean) => {
370
385
  settledSignature.value = undefined
371
386
  const signature = targetSignature.value
387
+ resultState.value = 'success'
372
388
  samplingScheduler.schedule(async (token) => {
373
389
  try {
374
390
  await runSampling(token, hysteresis)
@@ -410,6 +426,12 @@ export function useWaveformRenderSampling(context: SamplingContext) {
410
426
  onScopeDispose(() => session.dispose())
411
427
 
412
428
  return {
429
+ requestSignature: targetSignature,
430
+ state: computed(() =>
431
+ settledSignature.value === targetSignature.value && !session.autoModeSettleTimer
432
+ ? resultState.value
433
+ : 'pending',
434
+ ),
413
435
  linePointOverrides: context.linePointOverrides,
414
436
  ready: () => settledSignature.value === targetSignature.value && !session.autoModeSettleTimer,
415
437
  }
@@ -1,3 +1,4 @@
1
+ import type { WaveformAnimationProps } from '../../animation/types'
1
2
  import type { WaveformToolbarOptions, WaveformControlState } from '../../types/controls'
2
3
  import type {
3
4
  WaveformAnnotation,
@@ -26,7 +27,7 @@ import type {
26
27
  } from '../data/types'
27
28
  import type { WaveformGridOptions } from './grid'
28
29
 
29
- export interface WaveformChartProps {
30
+ export interface WaveformChartProps extends WaveformAnimationProps {
30
31
  toolbar?: boolean | WaveformToolbarOptions
31
32
  data: WaveformData
32
33
  displayMode?: WaveformDisplayMode
@@ -7,6 +7,7 @@ import { waveformLineDasharray, waveformPointSeriesPath } from './seriesStyle'
7
7
 
8
8
  const props = defineProps<{
9
9
  track: TrackLayout
10
+ paginationOpacity?: number
10
11
  clipPathId: string
11
12
  }>()
12
13
 
@@ -51,11 +52,22 @@ const renderedSeriesPaths = computed<RenderedSeriesPath[]>(() =>
51
52
  >
52
53
  <g
53
54
  v-for="seriesPath in renderedSeriesPaths"
54
- :key="seriesPath.series.id"
55
+ :key="seriesPath.presentationKey ?? seriesPath.series.id"
56
+ :opacity="(seriesPath.opacity ?? 1) * (paginationOpacity ?? 1)"
57
+ :clip-path="seriesPath.animationClip ? `url(#${seriesPath.animationClip.id})` : undefined"
55
58
  class="waveform-track__series-item waveform-chart__series"
56
59
  :data-series-id="seriesPath.series.id"
57
60
  :data-series-name="seriesPath.series.name || undefined"
58
61
  >
62
+ <defs v-if="seriesPath.animationClip">
63
+ <clipPath :id="seriesPath.animationClip.id" clipPathUnits="userSpaceOnUse">
64
+ <rect
65
+ :x="seriesPath.animationClip.x"
66
+ :width="seriesPath.animationClip.width"
67
+ :height="track.height"
68
+ />
69
+ </clipPath>
70
+ </defs>
59
71
  <path
60
72
  v-if="seriesPath.path"
61
73
  class="waveform-track__line waveform-chart__line"
@@ -12,6 +12,7 @@ import WaveformTrackBackdrop from './WaveformTrackBackdrop.vue'
12
12
 
13
13
  interface Props {
14
14
  track: TrackLayout
15
+ paginationOpacity?: number
15
16
  clipPathId: string
16
17
  innerWidth: number
17
18
  zoomable: boolean
@@ -136,7 +137,11 @@ const frameInset = computed(() =>
136
137
  aria-hidden="true"
137
138
  />
138
139
 
139
- <WaveformSeriesLayer :track="track" :clip-path-id="clipPathId" />
140
+ <WaveformSeriesLayer
141
+ :track="track"
142
+ :clip-path-id="clipPathId"
143
+ :pagination-opacity="paginationOpacity"
144
+ />
140
145
  <WaveformZeroLines
141
146
  :track="track"
142
147
  :clip-path-id="clipPathId"
@@ -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
+ }