waveform-analysis 0.1.29 → 0.1.31
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 +8 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +937 -714
- package/dist/style.css +1 -1
- package/dist/types/components/WaveformChart.vue.d.ts +1 -0
- package/dist/types/components/core/constants.d.ts +2 -0
- package/dist/types/components/core/useWaveformChartController.d.ts +8 -15
- package/dist/types/components/core/useWaveformChartLifecycle.d.ts +4 -1
- package/dist/types/components/core/useWaveformData.d.ts +1 -1
- package/dist/types/components/core/useWaveformLayout.d.ts +0 -1
- package/dist/types/components/core/useWaveformPresentation.d.ts +5 -0
- package/dist/types/components/core/waveformChartTypes.d.ts +9 -5
- package/dist/types/components/data/types.d.ts +1 -1
- package/dist/types/components/index.d.ts +1 -1
- package/dist/types/components/interaction/pointerCapture.d.ts +1 -0
- package/dist/types/components/interaction/viewportInteractionState.d.ts +39 -0
- package/dist/types/components/interaction/zoomConstraints.d.ts +14 -0
- package/dist/types/components/waveform.d.ts +1 -1
- package/dist/types/core/rendering.d.ts +1 -4
- package/dist/types/core/renderingStrategies.d.ts +22 -0
- package/dist/types/demo/types.d.ts +4 -2
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types/chart.d.ts +7 -0
- package/dist/types/types/index.d.ts +1 -1
- package/package.json +5 -1
- package/src/App.vue +10 -4
- package/src/components/WaveformChart.vue +1 -0
- package/src/components/WaveformChartView.vue +15 -12
- package/src/components/core/constants.ts +3 -0
- package/src/components/core/useWaveformChartController.ts +7 -2
- package/src/components/core/useWaveformChartLifecycle.ts +47 -3
- package/src/components/core/useWaveformData.ts +6 -1
- package/src/components/core/useWaveformLayout.ts +0 -3
- package/src/components/core/useWaveformPresentation.ts +17 -2
- package/src/components/core/waveformChartTypes.ts +7 -3
- package/src/components/data/types.ts +1 -0
- package/src/components/index.ts +1 -0
- package/src/components/interaction/WaveformTooltip.test.ts +39 -0
- package/src/components/interaction/WaveformTooltip.vue +72 -22
- package/src/components/interaction/pointerCapture.ts +10 -0
- package/src/components/interaction/useWaveformHover.ts +1 -1
- package/src/components/interaction/useWaveformViewport.ts +86 -74
- package/src/components/interaction/useWaveformZoom.ts +46 -14
- package/src/components/interaction/viewportInteractionState.test.ts +101 -0
- package/src/components/interaction/viewportInteractionState.ts +89 -0
- package/src/components/interaction/zoomConstraints.ts +167 -0
- package/src/components/waveform.ts +1 -0
- package/src/components/waveformChartCases/sizingAndTitles.test.ts +59 -0
- package/src/components/waveformChartCases/viewportLifecycle.test.ts +132 -0
- package/src/components/waveformChartCases/zoomPointConstraints.test.ts +248 -0
- package/src/core/rendering.ts +11 -81
- package/src/core/renderingStrategies.test.ts +90 -0
- package/src/core/renderingStrategies.ts +131 -0
- package/src/data/simulatedWaveforms.ts +9 -10
- package/src/demo/DemoChartHost.vue +2 -2
- package/src/demo/DemoGridFrameControls.vue +29 -0
- package/src/demo/plotMarginControls.test.ts +29 -0
- package/src/demo/types.ts +4 -1
- package/src/index.ts +1 -0
- package/src/styles.css +1 -0
- package/src/types/chart.ts +8 -0
- package/src/types/index.ts +1 -0
- package/src/utils/formatters.test.ts +4 -3
- package/src/utils/formatters.ts +1 -2
- package/src/utils/sampling.ts +2 -1
|
@@ -78,12 +78,14 @@ export function useWaveformChartController(
|
|
|
78
78
|
},
|
|
79
79
|
{ deep: true },
|
|
80
80
|
)
|
|
81
|
-
const selection =
|
|
81
|
+
const selection = shallowRef<ViewportSelectionState | null>(null)
|
|
82
82
|
const spacePressed = ref(false)
|
|
83
83
|
const pointerInsideChart = ref(false)
|
|
84
|
+
let handleBeforeDataReferenceChange: () => void = () => undefined
|
|
84
85
|
let handleDataReferenceChange: () => void = () => undefined
|
|
85
86
|
const preparedSeries = usePreparedWaveformSeries(
|
|
86
87
|
() => props.data,
|
|
88
|
+
() => handleBeforeDataReferenceChange(),
|
|
87
89
|
() => handleDataReferenceChange(),
|
|
88
90
|
)
|
|
89
91
|
|
|
@@ -127,6 +129,7 @@ export function useWaveformChartController(
|
|
|
127
129
|
const {
|
|
128
130
|
chartSeries,
|
|
129
131
|
chartTracks,
|
|
132
|
+
trackLayouts,
|
|
130
133
|
gridOptions,
|
|
131
134
|
pageCount,
|
|
132
135
|
pagedTracks,
|
|
@@ -138,7 +141,6 @@ export function useWaveformChartController(
|
|
|
138
141
|
initialXDomain,
|
|
139
142
|
sharedZoomDomain,
|
|
140
143
|
resolveInitialTrackDomain,
|
|
141
|
-
trackLayouts,
|
|
142
144
|
annotationLayoutsForTrack,
|
|
143
145
|
resolveSeriesYScale,
|
|
144
146
|
} = layout
|
|
@@ -254,6 +256,7 @@ export function useWaveformChartController(
|
|
|
254
256
|
gridOptions,
|
|
255
257
|
chartSeries,
|
|
256
258
|
chartTracks,
|
|
259
|
+
trackLayouts,
|
|
257
260
|
innerWidth,
|
|
258
261
|
innerHeight,
|
|
259
262
|
activeInteractionMode,
|
|
@@ -261,6 +264,7 @@ export function useWaveformChartController(
|
|
|
261
264
|
internalHiddenSeriesIds,
|
|
262
265
|
independentTransforms,
|
|
263
266
|
independentYDomains,
|
|
267
|
+
resolveInitialTrackDomain,
|
|
264
268
|
annotationInteraction,
|
|
265
269
|
editorSeriesOptions,
|
|
266
270
|
isPresentationMode,
|
|
@@ -272,6 +276,7 @@ export function useWaveformChartController(
|
|
|
272
276
|
cancelPendingHover: hover.cancelPendingHover,
|
|
273
277
|
clearZoomBindings: zoom.clearZoomBindings,
|
|
274
278
|
})
|
|
279
|
+
handleBeforeDataReferenceChange = lifecycle.handleBeforeDataReferenceChange
|
|
275
280
|
handleDataReferenceChange = lifecycle.handleDataReferenceChange
|
|
276
281
|
|
|
277
282
|
return reactive({
|
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
|
|
12
12
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
|
13
13
|
import type { useWaveformAnnotationInteraction } from '../annotation'
|
|
14
|
-
import type { DisplaySeries, DisplayTrack } from './types'
|
|
14
|
+
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
|
15
15
|
import type { ResolvedWaveformChartProps, WaveformChartEmit } from './waveformChartTypes'
|
|
16
|
+
import { constrainZoomDomain, transformForDomain } from '../interaction/zoomConstraints'
|
|
16
17
|
|
|
17
18
|
interface LifecycleContext {
|
|
18
19
|
props: ResolvedWaveformChartProps
|
|
@@ -35,6 +36,7 @@ interface LifecycleContext {
|
|
|
35
36
|
gridOptions: ComputedRef<{ rowCount: number; columnCount: number }>
|
|
36
37
|
chartSeries: ComputedRef<DisplaySeries[]>
|
|
37
38
|
chartTracks: ComputedRef<DisplayTrack[]>
|
|
39
|
+
trackLayouts: ComputedRef<TrackLayout[]>
|
|
38
40
|
innerWidth: ComputedRef<number>
|
|
39
41
|
innerHeight: ComputedRef<number>
|
|
40
42
|
activeInteractionMode: ComputedRef<string | undefined>
|
|
@@ -42,6 +44,7 @@ interface LifecycleContext {
|
|
|
42
44
|
internalHiddenSeriesIds: Ref<Set<string>>
|
|
43
45
|
independentTransforms: ShallowRef<ZoomTransform[]>
|
|
44
46
|
independentYDomains: Ref<Record<number, [number, number]>>
|
|
47
|
+
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
|
45
48
|
annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
|
|
46
49
|
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
|
47
50
|
isPresentationMode: ComputedRef<boolean>
|
|
@@ -87,6 +90,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
87
90
|
gridOptions,
|
|
88
91
|
chartSeries,
|
|
89
92
|
chartTracks,
|
|
93
|
+
trackLayouts,
|
|
90
94
|
innerWidth,
|
|
91
95
|
innerHeight,
|
|
92
96
|
activeInteractionMode,
|
|
@@ -94,6 +98,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
94
98
|
internalHiddenSeriesIds,
|
|
95
99
|
independentTransforms,
|
|
96
100
|
independentYDomains,
|
|
101
|
+
resolveInitialTrackDomain,
|
|
97
102
|
annotationInteraction,
|
|
98
103
|
editorSeriesOptions,
|
|
99
104
|
isPresentationMode,
|
|
@@ -139,6 +144,27 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
139
144
|
emit('page-change', nextPage, pageCount.value)
|
|
140
145
|
}
|
|
141
146
|
|
|
147
|
+
let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
|
|
148
|
+
|
|
149
|
+
function handleBeforeDataReferenceChange() {
|
|
150
|
+
if (props.displayMode !== 'independent') return
|
|
151
|
+
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
|
152
|
+
const configuredDomain =
|
|
153
|
+
props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
|
|
154
|
+
props.initialXDomains?.[track.series.id] ??
|
|
155
|
+
props.initialXDomain
|
|
156
|
+
if (
|
|
157
|
+
!configuredDomain ||
|
|
158
|
+
!Number.isFinite(configuredDomain[0]) ||
|
|
159
|
+
!Number.isFinite(configuredDomain[1]) ||
|
|
160
|
+
configuredDomain[0] === configuredDomain[1]
|
|
161
|
+
) {
|
|
162
|
+
return undefined
|
|
163
|
+
}
|
|
164
|
+
return track.xScale.domain() as [number, number]
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
function handleDataReferenceChange() {
|
|
143
169
|
if (props.displayMode === 'independent') {
|
|
144
170
|
const currentTransforms = independentTransforms.value
|
|
@@ -148,7 +174,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
148
174
|
}
|
|
149
175
|
clearHover()
|
|
150
176
|
editorSeriesOptions.value = []
|
|
151
|
-
void nextTick(
|
|
177
|
+
void nextTick(() => {
|
|
178
|
+
if (props.displayMode === 'independent' && pendingIndependentXDomains) {
|
|
179
|
+
const nextTransforms = chartTracks.value.map(() => zoomIdentity)
|
|
180
|
+
trackLayouts.value.forEach((track) => {
|
|
181
|
+
const previousDomain = pendingIndependentXDomains?.[track.index]
|
|
182
|
+
if (!previousDomain) return
|
|
183
|
+
const boundary = resolveInitialTrackDomain(track)
|
|
184
|
+
const domain = constrainZoomDomain(previousDomain, boundary, [track.seriesList], props)
|
|
185
|
+
nextTransforms[track.index] = transformForDomain(domain, boundary, track.width)
|
|
186
|
+
})
|
|
187
|
+
independentTransforms.value = nextTransforms
|
|
188
|
+
pendingIndependentXDomains = undefined
|
|
189
|
+
void nextTick(configureZoom)
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
pendingIndependentXDomains = undefined
|
|
193
|
+
configureZoom()
|
|
194
|
+
})
|
|
152
195
|
}
|
|
153
196
|
|
|
154
197
|
watch(
|
|
@@ -158,6 +201,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
158
201
|
() => props.zoomable,
|
|
159
202
|
isPresentationMode,
|
|
160
203
|
() => props.minZoomSpan,
|
|
204
|
+
() => props.minVisiblePoints,
|
|
161
205
|
() => props.initialXDomain,
|
|
162
206
|
() => props.initialXDomains,
|
|
163
207
|
() => props.displayMode,
|
|
@@ -324,5 +368,5 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|
|
324
368
|
editorSeriesOptions.value = []
|
|
325
369
|
})
|
|
326
370
|
|
|
327
|
-
return { goToPage, handleDataReferenceChange }
|
|
371
|
+
return { goToPage, handleBeforeDataReferenceChange, handleDataReferenceChange }
|
|
328
372
|
}
|
|
@@ -62,9 +62,14 @@ export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSerie
|
|
|
62
62
|
})
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
export function usePreparedWaveformSeries(
|
|
65
|
+
export function usePreparedWaveformSeries(
|
|
66
|
+
data: () => WaveformData,
|
|
67
|
+
onBeforeDataChange: () => void,
|
|
68
|
+
onDataChange: () => void,
|
|
69
|
+
) {
|
|
66
70
|
const preparedSeries = shallowRef<PreparedWaveformSeries[]>(prepareWaveformSeries(data()))
|
|
67
71
|
watch(data, (nextData) => {
|
|
72
|
+
onBeforeDataChange()
|
|
68
73
|
preparedSeries.value = prepareWaveformSeries(nextData)
|
|
69
74
|
onDataChange()
|
|
70
75
|
})
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
normalizeGridOptions,
|
|
25
25
|
paginateSeries,
|
|
26
26
|
resolveGridCellGeometry,
|
|
27
|
-
X_AXIS_BAND,
|
|
28
27
|
} from './grid'
|
|
29
28
|
import {
|
|
30
29
|
buildTrackLayouts,
|
|
@@ -316,7 +315,6 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
316
315
|
)
|
|
317
316
|
: [],
|
|
318
317
|
)
|
|
319
|
-
const xAxisTitleY = computed(() => innerHeight.value + X_AXIS_BAND + 10)
|
|
320
318
|
const editorSeries = computed<AnnotationSeriesInfo | undefined>(() => {
|
|
321
319
|
const seriesId = annotationInteraction.editorDraft.value?.annotation.seriesId
|
|
322
320
|
const series = chartSeries.value.find((item) => item.id === seriesId)
|
|
@@ -360,7 +358,6 @@ export function useWaveformLayout(context: LayoutContext) {
|
|
|
360
358
|
resolveSeriesYScale,
|
|
361
359
|
annotationTrackLayouts,
|
|
362
360
|
renderedAnnotations,
|
|
363
|
-
xAxisTitleY,
|
|
364
361
|
editorSeries,
|
|
365
362
|
resolveFrameNumber,
|
|
366
363
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
TITLE_CHAR_WIDTH_RATIO,
|
|
8
8
|
TITLE_DEFAULT_FONT_SIZE,
|
|
9
9
|
TITLE_LINE_HEIGHT,
|
|
10
|
+
X_AXIS_TITLE_BOTTOM_OFFSET,
|
|
10
11
|
ZERO_LINE_DEFAULTS,
|
|
11
12
|
} from './constants'
|
|
12
13
|
import { calculateRotatedTitleLayout, TITLE_AREA_HORIZONTAL_PADDING } from './title'
|
|
@@ -144,11 +145,23 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
144
145
|
const titleAreaHeight = computed(() =>
|
|
145
146
|
titleAreaReserved.value ? titleLayout.value.areaHeight : 0,
|
|
146
147
|
)
|
|
147
|
-
const
|
|
148
|
+
const resolvePlotMargin = (value: number | undefined, fallback: number) =>
|
|
149
|
+
typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
|
|
150
|
+
const resolvedPlotMargin = computed(() => ({
|
|
151
|
+
top: resolvePlotMargin(props.plotMargin.top, margin.top),
|
|
152
|
+
bottom: resolvePlotMargin(props.plotMargin.bottom, margin.bottom),
|
|
153
|
+
}))
|
|
154
|
+
const chartTopMargin = computed(() => resolvedPlotMargin.value.top)
|
|
148
155
|
const drawingHeight = computed(() =>
|
|
149
156
|
Math.max(0, chartHeight.value - titleAreaHeight.value - paginationBandHeight.value),
|
|
150
157
|
)
|
|
151
|
-
const
|
|
158
|
+
const xAxisTitleY = computed(() => Math.max(0, drawingHeight.value - X_AXIS_TITLE_BOTTOM_OFFSET))
|
|
159
|
+
const innerHeight = computed(() =>
|
|
160
|
+
Math.max(
|
|
161
|
+
0,
|
|
162
|
+
drawingHeight.value - resolvedPlotMargin.value.top - resolvedPlotMargin.value.bottom,
|
|
163
|
+
),
|
|
164
|
+
)
|
|
152
165
|
const titleAreaStyle = computed<CSSProperties>(() => ({
|
|
153
166
|
height: `${titleAreaHeight.value}px`,
|
|
154
167
|
justifyContent:
|
|
@@ -193,8 +206,10 @@ export function useWaveformPresentation(context: PresentationContext) {
|
|
|
193
206
|
titleMeasureStyle,
|
|
194
207
|
titleLayout,
|
|
195
208
|
titleAreaHeight,
|
|
209
|
+
resolvedPlotMargin,
|
|
196
210
|
chartTopMargin,
|
|
197
211
|
drawingHeight,
|
|
212
|
+
xAxisTitleY,
|
|
198
213
|
innerHeight,
|
|
199
214
|
titleAreaStyle,
|
|
200
215
|
titleVisualStyle,
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
WaveformInteractionMode,
|
|
8
8
|
WaveformLegendOptions,
|
|
9
9
|
WaveformOverlayMode,
|
|
10
|
+
WaveformPlotMargin,
|
|
10
11
|
WaveformPoint,
|
|
11
12
|
WaveformRenderingOptions,
|
|
12
13
|
WaveformTitleOptions,
|
|
@@ -42,6 +43,7 @@ export interface WaveformChartProps {
|
|
|
42
43
|
interactionMode?: WaveformInteractionMode
|
|
43
44
|
grid?: WaveformGridOptions
|
|
44
45
|
rendering?: WaveformRenderingOptions
|
|
46
|
+
plotMargin?: WaveformPlotMargin
|
|
45
47
|
title?: WaveformTitleOptions
|
|
46
48
|
legend?: WaveformLegendOptions
|
|
47
49
|
hiddenSeriesIds?: string[]
|
|
@@ -65,6 +67,7 @@ type DefaultedProp =
|
|
|
65
67
|
| 'annotationsVisible'
|
|
66
68
|
| 'grid'
|
|
67
69
|
| 'rendering'
|
|
70
|
+
| 'plotMargin'
|
|
68
71
|
| 'legend'
|
|
69
72
|
| 'defaultHiddenSeriesIds'
|
|
70
73
|
| 'cleanView'
|
|
@@ -92,16 +95,17 @@ export interface WaveformChartEmit {
|
|
|
92
95
|
(event: 'page-change', page: number, pageCount: number): void
|
|
93
96
|
}
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
interface ViewportSelectionBase {
|
|
96
99
|
trackIndex: number
|
|
97
100
|
independent: boolean
|
|
98
|
-
overlay: SVGRectElement
|
|
99
101
|
startX: number
|
|
100
102
|
startY: number
|
|
101
103
|
currentX: number
|
|
102
104
|
currentY: number
|
|
103
105
|
pointerId: number
|
|
104
|
-
mode: 'box' | 'pan'
|
|
105
106
|
xDomain: [number, number]
|
|
106
107
|
yDomains: Record<string, [number, number]>
|
|
107
108
|
}
|
|
109
|
+
|
|
110
|
+
export type ViewportSelectionState =
|
|
111
|
+
(ViewportSelectionBase & { kind: 'box' }) | (ViewportSelectionBase & { kind: 'pan' })
|
package/src/components/index.ts
CHANGED
|
@@ -38,9 +38,48 @@ describe('WaveformTooltip', () => {
|
|
|
38
38
|
const tooltip = mountTooltip(100, 200).get('.waveform-tooltip')
|
|
39
39
|
|
|
40
40
|
expect(tooltip.attributes('style')).toContain('left: 8px')
|
|
41
|
+
expect(tooltip.attributes('style')).toContain('max-width: 184px')
|
|
41
42
|
expect(tooltip.attributes('style')).not.toContain('right:')
|
|
42
43
|
})
|
|
43
44
|
|
|
45
|
+
it('keeps short content content-sized while exposing the available width cap', () => {
|
|
46
|
+
const tooltip = mountTooltip(100).get('.waveform-tooltip')
|
|
47
|
+
|
|
48
|
+
expect(tooltip.attributes('style')).toContain('left: 112px')
|
|
49
|
+
expect(tooltip.attributes('style')).toContain('max-width: 280px')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('keeps long series content in a wrapping content container', () => {
|
|
53
|
+
const longName = 'ENG8KJXAc-very-long-series-name-10001'
|
|
54
|
+
const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
|
|
55
|
+
const wrapper = mount(WaveformTooltip, {
|
|
56
|
+
props: {
|
|
57
|
+
visible: true,
|
|
58
|
+
position: { x: 100, y: 100 },
|
|
59
|
+
timeUnit: 'ms',
|
|
60
|
+
hoveredPoint: pointWithErrors,
|
|
61
|
+
seriesPoints: [
|
|
62
|
+
{
|
|
63
|
+
trackIndex: 0,
|
|
64
|
+
name: longName,
|
|
65
|
+
color: '#f00',
|
|
66
|
+
unit: 'very-long-unit',
|
|
67
|
+
point: pointWithErrors,
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
containerWidth: 400,
|
|
71
|
+
containerHeight: 300,
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const tooltip = wrapper.get('.waveform-tooltip')
|
|
76
|
+
expect(tooltip.get('.waveform-tooltip__series-content').text()).toContain(longName)
|
|
77
|
+
expect(tooltip.get('.waveform-tooltip__series-content').classes()).toContain(
|
|
78
|
+
'waveform-tooltip__series-content',
|
|
79
|
+
)
|
|
80
|
+
expect(tooltip.attributes('style')).toContain('max-width: 280px')
|
|
81
|
+
})
|
|
82
|
+
|
|
44
83
|
it('shows resolved asymmetric errors beside the hovered value', () => {
|
|
45
84
|
const pointWithErrors = { x: 1, y: 12, error: 1, upperError: 2 }
|
|
46
85
|
const wrapper = mount(WaveformTooltip, {
|
|
@@ -33,24 +33,68 @@ const props = defineProps<Props>()
|
|
|
33
33
|
|
|
34
34
|
const tooltipGap = 12
|
|
35
35
|
const containerPadding = 8
|
|
36
|
-
const
|
|
36
|
+
const tooltipPlacementWidth = 238
|
|
37
|
+
const tooltipMaxWidth = 320
|
|
38
|
+
const tooltipHorizontalPadding = 20
|
|
39
|
+
const tooltipLineHeight = 20
|
|
40
|
+
|
|
41
|
+
function estimateLineCount(text: string, width: number): number {
|
|
42
|
+
const contentWidth = Math.max(1, width - tooltipHorizontalPadding - 14)
|
|
43
|
+
const charactersPerLine = Math.max(1, Math.floor(contentWidth / 7.2))
|
|
44
|
+
return Math.max(1, Math.ceil([...text].length / charactersPerLine))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatSeriesText(seriesPoint: SeriesPoint): string {
|
|
48
|
+
const error = formatError(seriesPoint.point)
|
|
49
|
+
return `${seriesPoint.name ? `${seriesPoint.name}: ` : ''}${formatTooltipNumber(seriesPoint.point.y)}${
|
|
50
|
+
seriesPoint.unit ? ` ${seriesPoint.unit}` : ''
|
|
51
|
+
}${error ? ` ${error}` : ''}`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function estimateTooltipHeight(width: number, timeText: string): number {
|
|
55
|
+
const seriesLines = props.seriesPoints.reduce(
|
|
56
|
+
(total, seriesPoint) => total + estimateLineCount(formatSeriesText(seriesPoint), width),
|
|
57
|
+
0,
|
|
58
|
+
)
|
|
59
|
+
return 16 + tooltipLineHeight * (estimateLineCount(timeText, width) + seriesLines) + 5
|
|
60
|
+
}
|
|
37
61
|
|
|
38
62
|
const tooltipStyle = computed(() => {
|
|
39
63
|
if (!props.visible || !props.hoveredPoint) return { display: 'none' }
|
|
40
64
|
|
|
41
|
-
const estimatedHeight = 44 + props.seriesPoints.length * 22
|
|
42
65
|
const rightPlacement = props.position.x + tooltipGap
|
|
43
|
-
const leftPlacement = props.position.x - tooltipGap -
|
|
66
|
+
const leftPlacement = props.position.x - tooltipGap - tooltipPlacementWidth
|
|
67
|
+
const rightAvailableWidth = props.containerWidth - containerPadding - rightPlacement
|
|
68
|
+
const leftAvailableWidth = props.position.x - tooltipGap - containerPadding
|
|
69
|
+
const availableWidth = Math.max(
|
|
70
|
+
1,
|
|
71
|
+
Math.min(tooltipMaxWidth, props.containerWidth - containerPadding * 2),
|
|
72
|
+
)
|
|
44
73
|
const horizontalStyle =
|
|
45
|
-
rightPlacement +
|
|
46
|
-
? {
|
|
74
|
+
rightPlacement + tooltipPlacementWidth <= props.containerWidth - containerPadding
|
|
75
|
+
? {
|
|
76
|
+
left: `${rightPlacement}px`,
|
|
77
|
+
maxWidth: `${Math.min(tooltipMaxWidth, rightAvailableWidth)}px`,
|
|
78
|
+
}
|
|
47
79
|
: leftPlacement >= containerPadding
|
|
48
|
-
? {
|
|
49
|
-
|
|
80
|
+
? {
|
|
81
|
+
right: `${props.containerWidth - props.position.x + tooltipGap}px`,
|
|
82
|
+
maxWidth: `${Math.min(tooltipMaxWidth, leftAvailableWidth)}px`,
|
|
83
|
+
}
|
|
84
|
+
: { left: `${containerPadding}px`, maxWidth: `${availableWidth}px` }
|
|
85
|
+
|
|
86
|
+
const maxWidth = Number.parseFloat(horizontalStyle.maxWidth)
|
|
87
|
+
const timeText = `${props.timeUnit}: ${formatTooltipTime(props.hoveredPoint.x, props.timeUnit)}`
|
|
50
88
|
|
|
51
89
|
return {
|
|
52
90
|
...horizontalStyle,
|
|
53
|
-
top: `${Math.max(
|
|
91
|
+
top: `${Math.max(
|
|
92
|
+
8,
|
|
93
|
+
Math.min(
|
|
94
|
+
props.position.y - 18,
|
|
95
|
+
props.containerHeight - estimateTooltipHeight(maxWidth, timeText) - 8,
|
|
96
|
+
),
|
|
97
|
+
)}px`,
|
|
54
98
|
}
|
|
55
99
|
})
|
|
56
100
|
|
|
@@ -76,11 +120,13 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
76
120
|
class="waveform-tooltip__series waveform-chart__tooltip-series"
|
|
77
121
|
>
|
|
78
122
|
<i :style="{ backgroundColor: seriesPoint.color }" />
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
123
|
+
<span class="waveform-tooltip__series-content">
|
|
124
|
+
<strong v-if="seriesPoint.name">{{ seriesPoint.name }}:</strong>
|
|
125
|
+
<span class="waveform-tooltip__value">
|
|
126
|
+
{{ formatTooltipNumber(seriesPoint.point.y)
|
|
127
|
+
}}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }}
|
|
128
|
+
<small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small>
|
|
129
|
+
</span>
|
|
84
130
|
</span>
|
|
85
131
|
</span>
|
|
86
132
|
</div>
|
|
@@ -93,8 +139,8 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
93
139
|
z-index: 2;
|
|
94
140
|
display: grid;
|
|
95
141
|
gap: 3px;
|
|
96
|
-
|
|
97
|
-
max-width:
|
|
142
|
+
width: max-content;
|
|
143
|
+
max-width: min(320px, calc(100% - 16px));
|
|
98
144
|
padding: 8px 10px;
|
|
99
145
|
color: #333;
|
|
100
146
|
font:
|
|
@@ -116,30 +162,34 @@ function formatError(point: WaveformPoint): string | null {
|
|
|
116
162
|
|
|
117
163
|
.waveform-tooltip__series {
|
|
118
164
|
display: grid;
|
|
119
|
-
grid-template-columns: 8px minmax(0, 1fr)
|
|
165
|
+
grid-template-columns: 8px minmax(0, 1fr);
|
|
120
166
|
gap: 6px;
|
|
121
|
-
align-items:
|
|
167
|
+
align-items: start;
|
|
122
168
|
}
|
|
123
169
|
|
|
124
170
|
.waveform-tooltip__series i {
|
|
171
|
+
flex: 0 0 auto;
|
|
125
172
|
width: 8px;
|
|
126
173
|
height: 8px;
|
|
174
|
+
margin-top: 4px;
|
|
127
175
|
border-radius: 50%;
|
|
128
176
|
}
|
|
129
177
|
|
|
178
|
+
.waveform-tooltip__series-content {
|
|
179
|
+
min-width: 0;
|
|
180
|
+
overflow-wrap: anywhere;
|
|
181
|
+
white-space: normal;
|
|
182
|
+
}
|
|
183
|
+
|
|
130
184
|
.waveform-tooltip__series strong {
|
|
131
|
-
overflow: hidden;
|
|
132
185
|
font-weight: 600;
|
|
133
|
-
text-overflow: ellipsis;
|
|
134
|
-
white-space: nowrap;
|
|
135
186
|
}
|
|
136
187
|
|
|
137
188
|
.waveform-tooltip__value {
|
|
138
|
-
|
|
189
|
+
overflow-wrap: anywhere;
|
|
139
190
|
}
|
|
140
191
|
|
|
141
192
|
.waveform-tooltip__series small {
|
|
142
193
|
color: #667085;
|
|
143
|
-
white-space: nowrap;
|
|
144
194
|
}
|
|
145
195
|
</style>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function tryReleasePointerCapture(
|
|
2
|
+
target: SVGRectElement | null | undefined,
|
|
3
|
+
pointerId: number,
|
|
4
|
+
): void {
|
|
5
|
+
try {
|
|
6
|
+
target?.releasePointerCapture?.(pointerId)
|
|
7
|
+
} catch {
|
|
8
|
+
// The target may already be detached or have released the pointer.
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -136,7 +136,7 @@ export function useWaveformHover(context: HoverContext) {
|
|
|
136
136
|
}
|
|
137
137
|
const handleSharedPointerMove = (event: PointerEvent) => {
|
|
138
138
|
if (isPresentationMode.value) return
|
|
139
|
-
if (selection.value
|
|
139
|
+
if (selection.value && !selection.value.independent) {
|
|
140
140
|
updateViewportDrag(event)
|
|
141
141
|
return
|
|
142
142
|
}
|