waveform-analysis 0.1.70 → 0.1.72
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 +244 -45
- package/dist/assets/workerRuntime-5p-EI-GL.js +1 -0
- package/dist/index.cjs +2 -1
- package/dist/index.js +2924 -1915
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +24 -6
- package/dist/types/components/annotation/WaveformAnnotationLayer.vue.d.ts +2 -0
- package/dist/types/components/controls/WaveformToolbar.vue.d.ts +7 -0
- package/dist/types/components/controls/chartExportOptions.d.ts +5 -0
- package/dist/types/components/controls/chartSnapshot.d.ts +2 -0
- package/dist/types/components/controls/snapshotTitle.d.ts +2 -0
- package/dist/types/components/controls/toolbarRegistry.d.ts +21 -0
- package/dist/types/components/controls/useChartCommands.d.ts +42 -0
- package/dist/types/components/controls/useChartImageExport.d.ts +17 -0
- package/dist/types/components/controls/useControlMode.d.ts +8 -0
- package/dist/types/components/core/latestTaskScheduler.d.ts +1 -1
- package/dist/types/components/core/layout.d.ts +5 -3
- package/dist/types/components/core/niceScale.d.ts +34 -0
- package/dist/types/components/core/pagination.d.ts +2 -2
- package/dist/types/components/core/trackLayoutBuilder.d.ts +4 -1
- package/dist/types/components/core/trackPagination.d.ts +1 -1
- package/dist/types/components/core/types.d.ts +3 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +24 -5
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +2 -2
- package/dist/types/components/core/useWaveformData.d.ts +1 -0
- package/dist/types/components/core/useWaveformLayout.d.ts +4 -1
- package/dist/types/components/core/useWaveformMeasurements.d.ts +2 -0
- package/dist/types/components/core/useWaveformRenderSampling.d.ts +1 -0
- package/dist/types/components/core/useWaveformViewportRestore.d.ts +5 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +5 -0
- package/dist/types/components/core/waveformSamplingSession.d.ts +24 -0
- package/dist/types/components/core/yAxisLabelStyle.d.ts +3 -0
- package/dist/types/components/core/yAxisLayoutMetrics.d.ts +2 -1
- package/dist/types/components/core/yAxisPadding.d.ts +7 -0
- package/dist/types/components/core/yAxisTitleLayout.d.ts +2 -0
- package/dist/types/components/interaction/useWaveformViewport.d.ts +1 -0
- package/dist/types/components/rendering/WaveformLegend.vue.d.ts +2 -2
- package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -3
- package/dist/types/components/rendering/WaveformTrackAxes.vue.d.ts +0 -1
- package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +1 -0
- package/dist/types/components/rendering/compactFrame.d.ts +5 -0
- package/dist/types/components/rendering/yAxisTickVisibility.d.ts +11 -0
- package/dist/types/core/lineBoundary.d.ts +4 -0
- package/dist/types/core/rendering.d.ts +4 -0
- package/dist/types/core/renderingStrategies.d.ts +2 -0
- package/dist/types/core/workerSampling/outputBoundaries.d.ts +4 -0
- package/dist/types/data/simulatedWaveforms.d.ts +1 -1
- package/dist/types/demo/DemoYAxisPaddingControls.vue.d.ts +11 -0
- package/dist/types/demo/types.d.ts +6 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/types/chart.d.ts +30 -8
- package/dist/types/types/controls.d.ts +69 -0
- package/dist/types/types/index.d.ts +2 -1
- package/dist/types/utils/formatters.d.ts +1 -0
- package/package.json +7 -6
- package/src/App.vue +58 -14
- package/src/components/WaveformChart.css +10 -1
- package/src/components/WaveformChart.vue +6 -0
- package/src/components/WaveformChartView.vue +27 -23
- package/src/components/annotation/WaveformAnnotationLayer.vue +6 -1
- package/src/components/annotation/useWaveformChartAnnotations.ts +6 -1
- package/src/components/controls/WaveformToolbar.vue +248 -0
- package/src/components/controls/chartExportOptions.ts +32 -0
- package/src/components/controls/chartSnapshot.ts +132 -0
- package/src/components/controls/snapshotTitle.ts +73 -0
- package/src/components/controls/toolbarRegistry.ts +92 -0
- package/src/components/controls/useChartCommands.ts +294 -0
- package/src/components/controls/useChartImageExport.ts +155 -0
- package/src/components/controls/useControlMode.ts +30 -0
- package/src/components/core/latestTaskScheduler.ts +14 -2
- package/src/components/core/layout.ts +35 -34
- package/src/components/core/niceScale.ts +151 -0
- package/src/components/core/pagination.ts +3 -16
- package/src/components/core/trackLayoutBuilder.ts +34 -17
- package/src/components/core/trackPagination.ts +3 -3
- package/src/components/core/types.ts +3 -0
- package/src/components/core/useWaveformChartController.ts +95 -10
- package/src/components/core/useWaveformChartLifecycle.ts +11 -111
- package/src/components/core/useWaveformData.ts +2 -0
- package/src/components/core/useWaveformLayout.ts +17 -5
- package/src/components/core/useWaveformMeasurements.ts +63 -0
- package/src/components/core/useWaveformRenderSampling.ts +80 -59
- package/src/components/core/useWaveformViewportRestore.ts +105 -0
- package/src/components/core/useWaveformYAxisLayout.ts +3 -2
- package/src/components/core/waveformChartTypes.ts +5 -0
- package/src/components/core/waveformSamplingSession.ts +49 -0
- package/src/components/core/workerSamplingClient.ts +38 -6
- package/src/components/core/yAxisLabelStyle.ts +10 -0
- package/src/components/core/yAxisLayoutMetrics.ts +10 -9
- package/src/components/core/yAxisPadding.ts +45 -0
- package/src/components/core/yAxisTitleLayout.ts +11 -12
- package/src/components/interaction/useWaveformViewport.ts +11 -2
- package/src/components/rendering/WaveformTrack.css +1 -2
- package/src/components/rendering/WaveformTrack.vue +4 -12
- package/src/components/rendering/WaveformTrackAxes.vue +48 -19
- package/src/components/rendering/WaveformTrackBackdrop.vue +8 -3
- package/src/components/rendering/compactFrame.ts +31 -0
- package/src/components/rendering/yAxisTickVisibility.ts +95 -0
- package/src/core/lineBoundary.ts +24 -0
- package/src/core/rendering.ts +48 -25
- package/src/core/renderingStrategies.ts +19 -13
- package/src/core/waveformPointSource.ts +23 -1
- package/src/core/workerSampling/outputBoundaries.ts +32 -0
- package/src/core/workerSampling/repository.ts +26 -18
- package/src/core/workerSampling/repositoryDataset.ts +33 -1
- package/src/core/workerSampling/workerRuntime.ts +13 -3
- package/src/data/simulatedWaveforms.ts +57 -2
- package/src/demo/DemoChartHost.vue +5 -1
- package/src/demo/DemoGridFrameControls.vue +2 -0
- package/src/demo/DemoViewControls.vue +5 -2
- package/src/demo/DemoYAxisPaddingControls.vue +46 -0
- package/src/demo/embedBridge.ts +85 -0
- package/src/demo/types.ts +6 -0
- package/src/index.ts +18 -0
- package/src/types/chart.ts +32 -8
- package/src/types/controls.ts +80 -0
- package/src/types/index.ts +18 -0
- package/src/utils/formatters.ts +2 -1
- package/src/views/WasmSamplingDemo.vue +8 -1
- package/dist/assets/workerRuntime-BcYng9TR.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waveform-analysis",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.72",
|
|
4
4
|
"main": "./dist/index.cjs",
|
|
5
5
|
"module": "./dist/index.js",
|
|
6
6
|
"types": "./dist/types/index.d.ts",
|
|
@@ -55,20 +55,21 @@
|
|
|
55
55
|
"scripts": {
|
|
56
56
|
"dev": "vite",
|
|
57
57
|
"build": "pnpm typecheck && pnpm build:lib && pnpm build:types && pnpm build:demo",
|
|
58
|
-
"build:wasm": "
|
|
58
|
+
"build:wasm": "node scripts/prepare-wasm.mjs --force",
|
|
59
59
|
"build:lib": "vite build --config vite.lib.config.ts",
|
|
60
60
|
"build:types": "node scripts/clean-types.mjs && vue-tsc -p tsconfig.lib.json && node scripts/prepare-package-types.mjs",
|
|
61
61
|
"build:demo": "vite build",
|
|
62
62
|
"preview": "vite preview --outDir dist-demo",
|
|
63
|
-
"typecheck": "pnpm
|
|
63
|
+
"typecheck": "pnpm prepare:wasm && vue-tsc -b",
|
|
64
64
|
"check:file-length": "node scripts/check-file-length.mjs",
|
|
65
65
|
"lint": "eslint . --max-warnings=0",
|
|
66
66
|
"lint:oxlint": "oxlint . --deny-warnings",
|
|
67
67
|
"lint:all": "pnpm lint:oxlint && pnpm lint",
|
|
68
68
|
"format": "prettier --write .",
|
|
69
69
|
"format:check": "prettier --check .",
|
|
70
|
-
"test": "pnpm
|
|
71
|
-
"test:coverage": "pnpm
|
|
72
|
-
"test:package": "node scripts/test-package.mjs"
|
|
70
|
+
"test": "pnpm prepare:wasm && vitest run",
|
|
71
|
+
"test:coverage": "pnpm prepare:wasm && vitest run --coverage",
|
|
72
|
+
"test:package": "node scripts/test-package.mjs",
|
|
73
|
+
"prepare:wasm": "node scripts/prepare-wasm.mjs"
|
|
73
74
|
}
|
|
74
75
|
}
|
package/src/App.vue
CHANGED
|
@@ -26,11 +26,23 @@ import DemoControlPanel from './demo/DemoControlPanel.vue'
|
|
|
26
26
|
import type { DemoChartModel, DemoControlPanelModel } from './demo/types'
|
|
27
27
|
import { useDemoXAxisLabelControls } from './demo/useDemoXAxisLabelControls'
|
|
28
28
|
|
|
29
|
-
const
|
|
30
|
-
const
|
|
29
|
+
const demoPreset = new URLSearchParams(window.location.search).get('demo')
|
|
30
|
+
const compactNineDemo = demoPreset === 'compact-nine'
|
|
31
|
+
const compactFourteenDemo = demoPreset === 'compact-fourteen'
|
|
32
|
+
const fullChartData = createSimulatedWaveformData(compactFourteenDemo ? 14 : undefined)
|
|
33
|
+
if (compactNineDemo && fullChartData.kind === 'series') {
|
|
34
|
+
const trackIds = [...new Set(fullChartData.series.map((series) => series.trackId ?? series.id))]
|
|
35
|
+
const demoTrackIds = new Set(trackIds.slice(0, 9))
|
|
36
|
+
fullChartData.series = fullChartData.series.filter((series) =>
|
|
37
|
+
demoTrackIds.has(series.trackId ?? series.id),
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
const displayMode = ref<WaveformDisplayMode>(
|
|
41
|
+
compactNineDemo || compactFourteenDemo ? 'compact' : 'independent',
|
|
42
|
+
)
|
|
31
43
|
const overlayMode = ref<WaveformOverlayMode>('single-axis')
|
|
32
|
-
const rowCount = ref(4)
|
|
33
|
-
const columnCount = ref(1)
|
|
44
|
+
const rowCount = ref(compactNineDemo ? 5 : 4)
|
|
45
|
+
const columnCount = ref(compactNineDemo || compactFourteenDemo ? 2 : 1)
|
|
34
46
|
const frameBorderColor = ref('#1f2937')
|
|
35
47
|
const frameBorderWidth = ref(2)
|
|
36
48
|
const frameBorderStyle = ref<NonNullable<WaveformFrameStyle['borderStyle']>>('solid')
|
|
@@ -42,6 +54,10 @@ const verticalGridVisible = ref(true)
|
|
|
42
54
|
const verticalGridColor = ref('#dfe5ef')
|
|
43
55
|
const xAxisLineVisible = ref(false)
|
|
44
56
|
const yAxisLineVisible = ref(false)
|
|
57
|
+
const upperPaddingEnabled = ref(true)
|
|
58
|
+
const lowerPaddingEnabled = ref(true)
|
|
59
|
+
const upperPaddingPercent = ref(10)
|
|
60
|
+
const lowerPaddingPercent = ref(10)
|
|
45
61
|
const { controlModel: xAxisLabelControlModel, xAxisLabelFormatter } = useDemoXAxisLabelControls()
|
|
46
62
|
const annotations = ref<WaveformAnnotation[]>([])
|
|
47
63
|
const annotationsVisible = ref(true)
|
|
@@ -49,13 +65,14 @@ const cleanView = ref(false)
|
|
|
49
65
|
const presentationMode = ref(false)
|
|
50
66
|
const showTooltip = ref(true)
|
|
51
67
|
const integerZoom = ref(true)
|
|
52
|
-
const plotMarginTop = ref(
|
|
53
|
-
const plotMarginBottom = ref(
|
|
68
|
+
const plotMarginTop = ref(10)
|
|
69
|
+
const plotMarginBottom = ref(44)
|
|
54
70
|
const zeroLineVisible = ref(true)
|
|
55
71
|
const zeroLineColor = ref('#ff0000')
|
|
56
72
|
const zeroLineWidth = ref(1)
|
|
57
73
|
const zeroLineOpacity = ref(0.5)
|
|
58
74
|
const zeroLineDash = ref('6 4')
|
|
75
|
+
const toolbarVisible = ref(true)
|
|
59
76
|
const interactionMode = ref<WaveformInteractionMode>('zoom')
|
|
60
77
|
const legendPosition = ref<WaveformLegendPosition>('top-right')
|
|
61
78
|
const legendOrientation = ref<WaveformLegendOrientation>('auto')
|
|
@@ -65,10 +82,10 @@ const titleVisible = ref(true)
|
|
|
65
82
|
const titleText = ref('模拟波形分析')
|
|
66
83
|
const titleAlign = ref<NonNullable<WaveformTitleOptions['align']>>('center')
|
|
67
84
|
const titleFontFamily = ref('"Microsoft YaHei", "微软雅黑", sans-serif')
|
|
68
|
-
const titleFontSize = ref(
|
|
85
|
+
const titleFontSize = ref(18)
|
|
69
86
|
const titleRotation = ref(0)
|
|
70
87
|
const titleColor = ref('#1f2937')
|
|
71
|
-
const titleBold = ref(
|
|
88
|
+
const titleBold = ref(true)
|
|
72
89
|
const titleItalic = ref(false)
|
|
73
90
|
const titleUnderline = ref(false)
|
|
74
91
|
const controlsOpen = ref(false)
|
|
@@ -123,7 +140,13 @@ const axes = computed<WaveformAxesOptions>(() => ({
|
|
|
123
140
|
lineVisible: xAxisLineVisible.value,
|
|
124
141
|
...(xAxisLabelFormatter.value ? { labelFormatter: xAxisLabelFormatter.value } : {}),
|
|
125
142
|
},
|
|
126
|
-
y: {
|
|
143
|
+
y: {
|
|
144
|
+
lineVisible: yAxisLineVisible.value,
|
|
145
|
+
upperPaddingEnabled: upperPaddingEnabled.value,
|
|
146
|
+
lowerPaddingEnabled: lowerPaddingEnabled.value,
|
|
147
|
+
upperPaddingRatio: upperPaddingPercent.value / 100,
|
|
148
|
+
lowerPaddingRatio: lowerPaddingPercent.value / 100,
|
|
149
|
+
},
|
|
127
150
|
}))
|
|
128
151
|
const zeroLine = computed<WaveformZeroLineOptions>(() => ({
|
|
129
152
|
visible: zeroLineVisible.value,
|
|
@@ -243,7 +266,17 @@ function mergeIndependentWindow(
|
|
|
243
266
|
}
|
|
244
267
|
}
|
|
245
268
|
|
|
269
|
+
let pendingCommandId: string | undefined
|
|
270
|
+
let pendingPayloads: WaveformZoomEndPayload[] = []
|
|
246
271
|
async function handleZoomEnd(payload: WaveformZoomEndPayload) {
|
|
272
|
+
if (payload.action === 'fit') return
|
|
273
|
+
if (payload.commandId && payload.commandId === pendingCommandId) {
|
|
274
|
+
pendingPayloads.push(payload)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
pendingCommandId = payload.commandId
|
|
278
|
+
const batch = [payload]
|
|
279
|
+
pendingPayloads = batch
|
|
247
280
|
// Demo-only sequence number cancellation. Production code should use AbortController
|
|
248
281
|
// to cancel in-flight requests when a newer zoom gesture arrives.
|
|
249
282
|
const requestSequence = ++zoomRequestSequence
|
|
@@ -252,11 +285,16 @@ async function handleZoomEnd(payload: WaveformZoomEndPayload) {
|
|
|
252
285
|
|
|
253
286
|
// Demo-only stand-in for the backend response. Production code should replace this
|
|
254
287
|
// with a request using payload.start/payload.end and the optional channel metadata.
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
288
|
+
let nextData = chartData.value
|
|
289
|
+
batch.forEach((item) => {
|
|
290
|
+
const responseData = filterWaveformData(fullChartData, item.start, item.end)
|
|
291
|
+
nextData =
|
|
292
|
+
item.trackIndex !== undefined && item.seriesIds?.length
|
|
293
|
+
? mergeIndependentWindow(nextData, responseData, item.seriesIds)
|
|
294
|
+
: responseData
|
|
295
|
+
})
|
|
296
|
+
chartData.value = nextData
|
|
297
|
+
pendingCommandId = undefined
|
|
260
298
|
}
|
|
261
299
|
|
|
262
300
|
function resetWaveformViewport() {
|
|
@@ -297,6 +335,7 @@ onMounted(() => window.addEventListener('keydown', handleWindowKeydown))
|
|
|
297
335
|
onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown))
|
|
298
336
|
|
|
299
337
|
const controlPanelModel = reactive({
|
|
338
|
+
toolbarVisible,
|
|
300
339
|
controlsOpen,
|
|
301
340
|
displayMode,
|
|
302
341
|
overlayMode,
|
|
@@ -324,6 +363,10 @@ const controlPanelModel = reactive({
|
|
|
324
363
|
verticalGridColor,
|
|
325
364
|
xAxisLineVisible,
|
|
326
365
|
yAxisLineVisible,
|
|
366
|
+
upperPaddingEnabled,
|
|
367
|
+
lowerPaddingEnabled,
|
|
368
|
+
upperPaddingPercent,
|
|
369
|
+
lowerPaddingPercent,
|
|
327
370
|
...xAxisLabelControlModel,
|
|
328
371
|
frameBorderColor,
|
|
329
372
|
frameBackgroundColor,
|
|
@@ -354,6 +397,7 @@ const controlPanelModel = reactive({
|
|
|
354
397
|
}) satisfies DemoControlPanelModel
|
|
355
398
|
|
|
356
399
|
const chartModel = reactive({
|
|
400
|
+
toolbarVisible,
|
|
357
401
|
data: displayChartData,
|
|
358
402
|
initialXDomain,
|
|
359
403
|
displayMode,
|
|
@@ -117,8 +117,17 @@
|
|
|
117
117
|
font-size: 12px;
|
|
118
118
|
}
|
|
119
119
|
|
|
120
|
+
.waveform-chart--interaction-pan:not(.waveform-chart--presentation) .waveform-chart__overlay {
|
|
121
|
+
cursor: grab;
|
|
122
|
+
}
|
|
123
|
+
.waveform-chart--interaction-pan.waveform-chart--panning .waveform-chart__overlay {
|
|
124
|
+
cursor: grabbing;
|
|
125
|
+
}
|
|
126
|
+
.waveform-chart--interaction-none .waveform-chart__overlay {
|
|
127
|
+
cursor: default;
|
|
128
|
+
}
|
|
129
|
+
|
|
120
130
|
.waveform-chart--edge-compact .waveform-chart__x-label {
|
|
121
|
-
font-size: 18px;
|
|
122
131
|
font-weight: 600;
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -38,6 +38,12 @@ const emit = defineEmits<WaveformChartEmit>()
|
|
|
38
38
|
const controller = useWaveformChartController(props as ResolvedWaveformChartProps, emit)
|
|
39
39
|
|
|
40
40
|
defineExpose({
|
|
41
|
+
exportImage: controller.exportImage,
|
|
42
|
+
zoomIn: controller.zoomIn,
|
|
43
|
+
zoomOut: controller.zoomOut,
|
|
44
|
+
fitToData: controller.fitToData,
|
|
45
|
+
setInteractionMode: controller.setInteractionMode,
|
|
46
|
+
getControlState: controller.getControlState,
|
|
41
47
|
resetViewport: controller.resetViewport,
|
|
42
48
|
setViewportDomain: controller.setViewportDomain,
|
|
43
49
|
})
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
+
import WaveformToolbar from './controls/WaveformToolbar.vue'
|
|
2
3
|
import { Pagination } from 'ant-design-vue'
|
|
3
|
-
import { toRefs } from 'vue'
|
|
4
|
+
import { computed, toRefs } from 'vue'
|
|
4
5
|
|
|
5
6
|
import { WaveformAnnotationContextMenu, WaveformAnnotationLayer } from './annotation'
|
|
6
7
|
import WaveformAnnotationEditor from './annotation/WaveformAnnotationEditor.vue'
|
|
7
8
|
import type { WaveformChartController } from './core/useWaveformChartController'
|
|
8
|
-
import type { TrackLayout } from './core/types'
|
|
9
9
|
import { WaveformHoverHost } from './interaction'
|
|
10
10
|
import { WaveformHoverLayer, WaveformLegend, WaveformTrack } from './rendering'
|
|
11
|
+
import { compactFramePath } from './rendering/compactFrame'
|
|
11
12
|
|
|
12
13
|
const props = defineProps<{ controller: WaveformChartController }>()
|
|
14
|
+
const compactFrameInset = computed(() => {
|
|
15
|
+
const width = props.controller.frameStyle?.borderWidth
|
|
16
|
+
return props.controller.isEdgeCompact
|
|
17
|
+
? (typeof width === 'number' && Number.isFinite(width) && width >= 0 ? width : 1) / 2
|
|
18
|
+
: 0
|
|
19
|
+
})
|
|
13
20
|
const {
|
|
14
21
|
displayMode,
|
|
15
22
|
activeInteractionMode,
|
|
@@ -111,21 +118,6 @@ function handleChartPointerLeave() {
|
|
|
111
118
|
pointerInsideChart.value = false
|
|
112
119
|
handlePointerLeave.value()
|
|
113
120
|
}
|
|
114
|
-
|
|
115
|
-
function sharesFrameTop(track: TrackLayout): boolean {
|
|
116
|
-
return (
|
|
117
|
-
displayMode.value === 'compact' &&
|
|
118
|
-
trackLayouts.value.some(
|
|
119
|
-
(above) =>
|
|
120
|
-
!above.isEmpty &&
|
|
121
|
-
above !== track &&
|
|
122
|
-
above.column === track.column &&
|
|
123
|
-
Math.abs(above.left - track.left) < 1e-6 &&
|
|
124
|
-
Math.abs(above.width - track.width) < 1e-6 &&
|
|
125
|
-
Math.abs(above.top + above.height - track.top) < 1e-6,
|
|
126
|
-
)
|
|
127
|
-
)
|
|
128
|
-
}
|
|
129
121
|
</script>
|
|
130
122
|
|
|
131
123
|
<template>
|
|
@@ -205,7 +197,7 @@ function sharesFrameTop(track: TrackLayout): boolean {
|
|
|
205
197
|
|
|
206
198
|
<g :transform="`translate(${resolvedChartLeftMargin}, ${chartTopMargin})`">
|
|
207
199
|
<g
|
|
208
|
-
v-if="displayMode !== 'compact' && !isCleanView"
|
|
200
|
+
v-if="displayMode !== 'compact' && !hasWaveformData && !isCleanView"
|
|
209
201
|
class="waveform-chart__grid-slots"
|
|
210
202
|
aria-hidden="true"
|
|
211
203
|
>
|
|
@@ -246,21 +238,28 @@ function sharesFrameTop(track: TrackLayout): boolean {
|
|
|
246
238
|
v-for="track in trackLayouts"
|
|
247
239
|
:key="`${track.index}-${track.id}`"
|
|
248
240
|
:track="track"
|
|
241
|
+
:frame-path="
|
|
242
|
+
displayMode === 'compact'
|
|
243
|
+
? compactFramePath(track, trackLayouts, compactFrameInset)
|
|
244
|
+
: undefined
|
|
245
|
+
"
|
|
249
246
|
:clip-path-id="clipPathId"
|
|
250
247
|
:inner-width="innerWidth"
|
|
251
248
|
:zoomable="zoomable"
|
|
252
249
|
:interactive="!isPresentationMode"
|
|
253
250
|
:display-mode="displayMode"
|
|
254
251
|
:interaction-mode="activeInteractionMode"
|
|
255
|
-
:frame-number="
|
|
252
|
+
:frame-number="
|
|
253
|
+
resolveFrameNumber(track.id, (currentPage - 1) * getPageSize(gridOptions) + track.index)
|
|
254
|
+
"
|
|
256
255
|
:frame-style="frameStyle"
|
|
257
256
|
:axes="axes"
|
|
258
257
|
:show-axis-units="unitDisplayMode === 'axis'"
|
|
259
|
-
:show-label-units="
|
|
258
|
+
:show-label-units="
|
|
259
|
+
unitDisplayMode === 'channel-label-or-legend' || unitDisplayMode === 'channel-label'
|
|
260
|
+
"
|
|
260
261
|
:hide-y-axis-titles="hideYAxisTitles"
|
|
261
|
-
:contain-y-axis-endpoints="isEdgeCompact"
|
|
262
262
|
:contain-frame-stroke="isEdgeCompact"
|
|
263
|
-
:omit-frame-top="sharesFrameTop(track)"
|
|
264
263
|
:clean-view="isCleanView"
|
|
265
264
|
:zero-line="resolvedZeroLine"
|
|
266
265
|
:time-unit="timeUnit"
|
|
@@ -292,10 +291,12 @@ function sharesFrameTop(track: TrackLayout): boolean {
|
|
|
292
291
|
/>
|
|
293
292
|
|
|
294
293
|
<WaveformAnnotationLayer
|
|
294
|
+
:cancel-key="activeInteractionMode"
|
|
295
|
+
:data-version="controller.data"
|
|
295
296
|
v-if="!isCleanView"
|
|
296
297
|
:annotations="renderedAnnotations"
|
|
297
298
|
:visible="annotationsVisible"
|
|
298
|
-
:interactive="!isPresentationMode"
|
|
299
|
+
:interactive="!isPresentationMode && activeInteractionMode !== 'none'"
|
|
299
300
|
@contextmenu="handleExistingAnnotationContextMenu"
|
|
300
301
|
@drag-start="beginAnnotationDrag"
|
|
301
302
|
@move="handleAnnotationMove"
|
|
@@ -331,6 +332,7 @@ function sharesFrameTop(track: TrackLayout): boolean {
|
|
|
331
332
|
<text
|
|
332
333
|
v-if="resolvedXLabel && !isCleanView"
|
|
333
334
|
class="waveform-chart__label waveform-chart__x-label"
|
|
335
|
+
:style="isEdgeCompact ? { fontSize: titleTextStyle.fontSize } : undefined"
|
|
334
336
|
:x="plotCenterX"
|
|
335
337
|
:y="xAxisTitleY"
|
|
336
338
|
text-anchor="middle"
|
|
@@ -349,6 +351,8 @@ function sharesFrameTop(track: TrackLayout): boolean {
|
|
|
349
351
|
</text>
|
|
350
352
|
</svg>
|
|
351
353
|
|
|
354
|
+
<WaveformToolbar :controller="controller" />
|
|
355
|
+
|
|
352
356
|
<Pagination
|
|
353
357
|
v-if="paginationVisible"
|
|
354
358
|
class="waveform-chart__pagination"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed, ref, watch } from 'vue'
|
|
2
|
+
import { computed, ref, watch, onBeforeUnmount } from 'vue'
|
|
3
3
|
|
|
4
4
|
import type { RenderedAnnotation } from './types'
|
|
5
5
|
import { ANNOTATION_TEXT_FONT, ANNOTATION_TEXT_LINE_HEIGHT } from './markup'
|
|
@@ -8,6 +8,8 @@ interface Props {
|
|
|
8
8
|
annotations: RenderedAnnotation[]
|
|
9
9
|
visible: boolean
|
|
10
10
|
interactive?: boolean
|
|
11
|
+
cancelKey?: string
|
|
12
|
+
dataVersion?: unknown
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -211,6 +213,9 @@ watch(
|
|
|
211
213
|
},
|
|
212
214
|
)
|
|
213
215
|
|
|
216
|
+
watch([() => props.cancelKey, () => props.dataVersion, () => props.visible], cancelActiveDrag)
|
|
217
|
+
onBeforeUnmount(cancelActiveDrag)
|
|
218
|
+
|
|
214
219
|
function handleContextMenu(annotationId: string, event: MouseEvent) {
|
|
215
220
|
if (!props.interactive) return
|
|
216
221
|
event.preventDefault()
|
|
@@ -282,7 +282,12 @@ export function useWaveformChartAnnotations(context: AnnotationContext) {
|
|
|
282
282
|
}
|
|
283
283
|
|
|
284
284
|
function handleAnnotationContextMenu(event: MouseEvent, trackIndex?: number) {
|
|
285
|
-
if (
|
|
285
|
+
if (
|
|
286
|
+
isPresentationMode.value ||
|
|
287
|
+
!props.annotationsVisible ||
|
|
288
|
+
activeInteractionMode.value === 'none'
|
|
289
|
+
)
|
|
290
|
+
return
|
|
286
291
|
event.preventDefault()
|
|
287
292
|
event.stopPropagation()
|
|
288
293
|
const candidateContext = resolveAnnotationCandidates(event, trackIndex)
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, onBeforeUnmount, ref, watch } from 'vue'
|
|
3
|
+
import type { WaveformChartController } from '../core/useWaveformChartController'
|
|
4
|
+
import { resolveToolbar } from './toolbarRegistry'
|
|
5
|
+
const props = defineProps<{ controller: WaveformChartController }>()
|
|
6
|
+
const config = computed(() => resolveToolbar(props.controller.toolbar))
|
|
7
|
+
const compact = computed(() => props.controller.chartWidth < 440)
|
|
8
|
+
const expanded = ref(false)
|
|
9
|
+
const selected = ref('')
|
|
10
|
+
const menu = ref(false)
|
|
11
|
+
const busy = ref(false)
|
|
12
|
+
const feedback = ref('')
|
|
13
|
+
let disposed = false
|
|
14
|
+
onBeforeUnmount(() => {
|
|
15
|
+
disposed = true
|
|
16
|
+
})
|
|
17
|
+
watch(
|
|
18
|
+
() => props.controller.controlState,
|
|
19
|
+
(state) => {
|
|
20
|
+
if (!state.independent || !state.targets.some((t) => t.trackId === selected.value))
|
|
21
|
+
selected.value = ''
|
|
22
|
+
},
|
|
23
|
+
)
|
|
24
|
+
watch(
|
|
25
|
+
() => config.value.visible,
|
|
26
|
+
() => {
|
|
27
|
+
menu.value = false
|
|
28
|
+
},
|
|
29
|
+
)
|
|
30
|
+
async function download(format: 'png' | 'svg') {
|
|
31
|
+
menu.value = false
|
|
32
|
+
busy.value = true
|
|
33
|
+
feedback.value = ''
|
|
34
|
+
try {
|
|
35
|
+
const blob = await props.controller.exportImage({ format })
|
|
36
|
+
if (disposed) return
|
|
37
|
+
const url = URL.createObjectURL(blob)
|
|
38
|
+
const link = document.createElement('a')
|
|
39
|
+
try {
|
|
40
|
+
link.href = url
|
|
41
|
+
link.download = `waveform.${format}`
|
|
42
|
+
document.body.appendChild(link)
|
|
43
|
+
link.click()
|
|
44
|
+
} finally {
|
|
45
|
+
link.remove()
|
|
46
|
+
URL.revokeObjectURL(url)
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
if (!disposed) feedback.value = error instanceof Error ? error.message : '导出失败,请重试'
|
|
50
|
+
} finally {
|
|
51
|
+
busy.value = false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function toggleExpanded() {
|
|
55
|
+
expanded.value = !expanded.value
|
|
56
|
+
menu.value = false
|
|
57
|
+
}
|
|
58
|
+
function escape() {
|
|
59
|
+
menu.value = false
|
|
60
|
+
expanded.value = false
|
|
61
|
+
props.controller.cancelViewportDrag()
|
|
62
|
+
}
|
|
63
|
+
</script>
|
|
64
|
+
<template>
|
|
65
|
+
<div
|
|
66
|
+
v-if="config.visible"
|
|
67
|
+
class="waveform-toolbar"
|
|
68
|
+
:class="[
|
|
69
|
+
`waveform-toolbar--${config.display}`,
|
|
70
|
+
`waveform-toolbar--${config.position}`,
|
|
71
|
+
{ 'waveform-toolbar--collapsed': compact && !expanded },
|
|
72
|
+
]"
|
|
73
|
+
:style="{ top: `${controller.titleAreaHeight + 4}px` }"
|
|
74
|
+
role="toolbar"
|
|
75
|
+
aria-label="波形工具栏"
|
|
76
|
+
@pointerdown.stop
|
|
77
|
+
@click.stop
|
|
78
|
+
@dblclick.stop
|
|
79
|
+
@wheel.stop
|
|
80
|
+
@contextmenu.stop
|
|
81
|
+
@keydown.esc.stop="escape"
|
|
82
|
+
>
|
|
83
|
+
<button
|
|
84
|
+
v-if="compact"
|
|
85
|
+
type="button"
|
|
86
|
+
class="waveform-toolbar__toggle"
|
|
87
|
+
aria-label="图表工具"
|
|
88
|
+
:aria-expanded="expanded"
|
|
89
|
+
@click="toggleExpanded"
|
|
90
|
+
>
|
|
91
|
+
工具
|
|
92
|
+
</button>
|
|
93
|
+
<select v-if="controller.controlState.independent" v-model="selected" aria-label="操作图框">
|
|
94
|
+
<option value="">当前页全部</option>
|
|
95
|
+
<option
|
|
96
|
+
v-for="target in controller.controlState.targets"
|
|
97
|
+
:key="target.trackId"
|
|
98
|
+
:value="target.trackId"
|
|
99
|
+
>
|
|
100
|
+
{{ target.label }}
|
|
101
|
+
</option>
|
|
102
|
+
</select>
|
|
103
|
+
<span v-else class="waveform-toolbar__scope">共享范围</span>
|
|
104
|
+
<div
|
|
105
|
+
v-for="(group, index) in config.groups"
|
|
106
|
+
:key="index"
|
|
107
|
+
class="waveform-toolbar__group"
|
|
108
|
+
:data-group="group[0]?.group"
|
|
109
|
+
>
|
|
110
|
+
<button
|
|
111
|
+
v-for="item in group"
|
|
112
|
+
:key="item.id"
|
|
113
|
+
type="button"
|
|
114
|
+
:data-command="item.id"
|
|
115
|
+
:title="item.label"
|
|
116
|
+
:aria-label="item.label"
|
|
117
|
+
:aria-pressed="item.group === 'mode' ? item.active(controller.controlState) : undefined"
|
|
118
|
+
:aria-expanded="item.id === 'export' ? menu : undefined"
|
|
119
|
+
:disabled="
|
|
120
|
+
!item.enabled(controller.controlState, selected || undefined) ||
|
|
121
|
+
(item.id === 'export' && busy)
|
|
122
|
+
"
|
|
123
|
+
@click="item.id === 'export' ? (menu = !menu) : item.run(controller, selected || undefined)"
|
|
124
|
+
>
|
|
125
|
+
<svg viewBox="0 0 24 24" aria-hidden="true"><path :d="item.icon" /></svg>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div v-if="menu" class="waveform-toolbar__menu" role="group" aria-label="图片格式">
|
|
129
|
+
<button type="button" @click="download('png')">下载 PNG</button>
|
|
130
|
+
<button type="button" @click="download('svg')">下载 SVG</button>
|
|
131
|
+
</div>
|
|
132
|
+
<span v-if="busy || feedback" class="waveform-toolbar__feedback" role="status">{{
|
|
133
|
+
busy ? '正在导出…' : feedback
|
|
134
|
+
}}</span>
|
|
135
|
+
</div>
|
|
136
|
+
</template>
|
|
137
|
+
<style scoped>
|
|
138
|
+
.waveform-toolbar {
|
|
139
|
+
position: absolute;
|
|
140
|
+
z-index: 10;
|
|
141
|
+
top: 4px;
|
|
142
|
+
right: 6px;
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-wrap: wrap;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 4px;
|
|
147
|
+
max-width: calc(100% - 12px);
|
|
148
|
+
padding: 4px;
|
|
149
|
+
background: rgb(255 255 255 / 96%);
|
|
150
|
+
border: 1px solid #d0d5dd;
|
|
151
|
+
border-radius: 6px;
|
|
152
|
+
box-shadow: 0 2px 8px #0001;
|
|
153
|
+
}
|
|
154
|
+
.waveform-toolbar--collapsed > :not(.waveform-toolbar__toggle):not(.waveform-toolbar__feedback) {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
.waveform-toolbar--top-left {
|
|
158
|
+
left: 6px;
|
|
159
|
+
right: auto;
|
|
160
|
+
}
|
|
161
|
+
.waveform-toolbar--hover {
|
|
162
|
+
opacity: 0;
|
|
163
|
+
pointer-events: none;
|
|
164
|
+
}
|
|
165
|
+
.waveform-chart:hover .waveform-toolbar--hover,
|
|
166
|
+
.waveform-toolbar:focus-within,
|
|
167
|
+
.waveform-toolbar:has(.waveform-toolbar__menu),
|
|
168
|
+
.waveform-toolbar:has(.waveform-toolbar__feedback) {
|
|
169
|
+
opacity: 1;
|
|
170
|
+
pointer-events: auto;
|
|
171
|
+
}
|
|
172
|
+
.waveform-toolbar__group {
|
|
173
|
+
display: flex;
|
|
174
|
+
gap: 2px;
|
|
175
|
+
}
|
|
176
|
+
.waveform-toolbar__group + .waveform-toolbar__group {
|
|
177
|
+
border-left: 1px solid #d0d5dd;
|
|
178
|
+
padding-left: 4px;
|
|
179
|
+
}
|
|
180
|
+
button {
|
|
181
|
+
display: inline-flex;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
min-width: 30px;
|
|
185
|
+
min-height: 30px;
|
|
186
|
+
border: 0;
|
|
187
|
+
border-radius: 4px;
|
|
188
|
+
background: transparent;
|
|
189
|
+
color: #344054;
|
|
190
|
+
cursor: pointer;
|
|
191
|
+
}
|
|
192
|
+
button:hover,
|
|
193
|
+
button[aria-pressed='true'] {
|
|
194
|
+
background: #e6f4ff;
|
|
195
|
+
color: #0960bd;
|
|
196
|
+
}
|
|
197
|
+
button:disabled {
|
|
198
|
+
opacity: 0.35;
|
|
199
|
+
cursor: default;
|
|
200
|
+
}
|
|
201
|
+
button:focus-visible,
|
|
202
|
+
select:focus-visible {
|
|
203
|
+
outline: 2px solid #0960bd;
|
|
204
|
+
outline-offset: 1px;
|
|
205
|
+
}
|
|
206
|
+
svg {
|
|
207
|
+
width: 18px;
|
|
208
|
+
height: 18px;
|
|
209
|
+
fill: none;
|
|
210
|
+
stroke: currentColor;
|
|
211
|
+
stroke-width: 1.6;
|
|
212
|
+
stroke-linecap: round;
|
|
213
|
+
stroke-linejoin: round;
|
|
214
|
+
}
|
|
215
|
+
select {
|
|
216
|
+
min-width: 0;
|
|
217
|
+
max-width: 120px;
|
|
218
|
+
height: 30px;
|
|
219
|
+
border: 1px solid #d0d5dd;
|
|
220
|
+
border-radius: 4px;
|
|
221
|
+
background: white;
|
|
222
|
+
}
|
|
223
|
+
.waveform-toolbar__scope {
|
|
224
|
+
font-size: 12px;
|
|
225
|
+
color: #667085;
|
|
226
|
+
}
|
|
227
|
+
.waveform-toolbar__menu {
|
|
228
|
+
position: absolute;
|
|
229
|
+
right: 0;
|
|
230
|
+
top: 100%;
|
|
231
|
+
padding: 6px;
|
|
232
|
+
border: 1px solid #d0d5dd;
|
|
233
|
+
border-radius: 4px;
|
|
234
|
+
background: white;
|
|
235
|
+
display: flex;
|
|
236
|
+
}
|
|
237
|
+
.waveform-toolbar__feedback {
|
|
238
|
+
flex-basis: 100%;
|
|
239
|
+
font-size: 12px;
|
|
240
|
+
max-width: 300px;
|
|
241
|
+
}
|
|
242
|
+
@media (hover: none) {
|
|
243
|
+
.waveform-toolbar--hover {
|
|
244
|
+
opacity: 1;
|
|
245
|
+
pointer-events: auto;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
</style>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
|
2
|
+
|
|
3
|
+
/** Presentation changes invalidate an in-flight image snapshot, without serializing input data. */
|
|
4
|
+
export function chartExportOptions(props: ResolvedWaveformChartProps) {
|
|
5
|
+
const keys = [
|
|
6
|
+
'annotations',
|
|
7
|
+
'annotationsVisible',
|
|
8
|
+
'title',
|
|
9
|
+
'axes',
|
|
10
|
+
'frameStyle',
|
|
11
|
+
'frameNumber',
|
|
12
|
+
'frameNumbers',
|
|
13
|
+
'legend',
|
|
14
|
+
'cleanView',
|
|
15
|
+
'rendering',
|
|
16
|
+
'displayMode',
|
|
17
|
+
'overlayMode',
|
|
18
|
+
'initialXDomain',
|
|
19
|
+
'initialXDomains',
|
|
20
|
+
'yDomain',
|
|
21
|
+
'yDomains',
|
|
22
|
+
'grid',
|
|
23
|
+
'plotMargin',
|
|
24
|
+
'xLabel',
|
|
25
|
+
'yLabel',
|
|
26
|
+
'timeUnit',
|
|
27
|
+
'zeroLine',
|
|
28
|
+
'layoutPreset',
|
|
29
|
+
'unitDisplayMode',
|
|
30
|
+
] as const satisfies readonly (keyof ResolvedWaveformChartProps)[]
|
|
31
|
+
return Object.fromEntries(keys.map((key) => [key, props[key]]))
|
|
32
|
+
}
|