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.
- package/README.md +246 -39
- package/dist/assets/workerRuntime-BJiRmIZD.js +2446 -0
- package/dist/index.cjs +1 -2
- package/dist/index.js +1 -7119
- package/dist/style.css +1 -1
- package/dist/types/animation/WaveformAnimationController.d.ts +30 -0
- package/dist/types/animation/core/Animation.d.ts +13 -0
- package/dist/types/animation/core/Animator.d.ts +23 -0
- package/dist/types/animation/core/easing.d.ts +3 -0
- package/dist/types/animation/core/errors.d.ts +4 -0
- package/dist/types/animation/core/handle.d.ts +7 -0
- package/dist/types/animation/core/owner.d.ts +3 -0
- package/dist/types/animation/core/propertyGroups.d.ts +17 -0
- package/dist/types/animation/core/task.d.ts +29 -0
- package/dist/types/animation/core/types.d.ts +74 -0
- package/dist/types/animation/core/validation.d.ts +30 -0
- package/dist/types/animation/core/values.d.ts +8 -0
- package/dist/types/animation/presets/annotation.d.ts +4 -0
- package/dist/types/animation/presets/lineDrawing.d.ts +7 -0
- package/dist/types/animation/presets/shared.d.ts +14 -0
- package/dist/types/animation/presets/transitions.d.ts +4 -0
- package/dist/types/animation/types.d.ts +84 -0
- package/dist/types/components/WaveformChart.vue.d.ts +3 -2
- package/dist/types/components/animation/annotationPresentation.d.ts +17 -0
- package/dist/types/components/animation/chartExportVersion.d.ts +2 -0
- package/dist/types/components/animation/config.d.ts +31 -0
- package/dist/types/components/animation/context.d.ts +20 -0
- package/dist/types/components/animation/geometry.d.ts +10 -0
- package/dist/types/components/animation/paginationPresentation.d.ts +13 -0
- package/dist/types/components/animation/pathBuffer.d.ts +4 -0
- package/dist/types/components/animation/trackPresentation.d.ts +35 -0
- package/dist/types/components/animation/useWaveformAnimations.d.ts +27 -0
- package/dist/types/components/annotation/WaveformAnnotationContextMenu.vue.d.ts +2 -2
- package/dist/types/components/annotation/types.d.ts +7 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +2 -0
- package/dist/types/components/core/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/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/index.d.ts +9 -0
- package/dist/types/views/AnimationDemo.vue.d.ts +3 -0
- package/package.json +5 -2
- 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/trackLayoutBuilder.ts +9 -18
- package/src/components/core/types.ts +4 -0
- package/src/components/core/useWaveformChartController.ts +24 -20
- package/src/components/core/useWaveformRenderSampling.ts +23 -1
- package/src/components/core/waveformChartTypes.ts +2 -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/index.ts +52 -0
- package/src/router.ts +1 -0
- package/src/views/AnimationDemo.vue +129 -0
- package/dist/assets/workerRuntime-5p-EI-GL.js +0 -1
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
|
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')
|
|
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
|
|
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 {
|
|
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
|
+
}
|