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.
Files changed (65) hide show
  1. package/README.md +8 -0
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +937 -714
  4. package/dist/style.css +1 -1
  5. package/dist/types/components/WaveformChart.vue.d.ts +1 -0
  6. package/dist/types/components/core/constants.d.ts +2 -0
  7. package/dist/types/components/core/useWaveformChartController.d.ts +8 -15
  8. package/dist/types/components/core/useWaveformChartLifecycle.d.ts +4 -1
  9. package/dist/types/components/core/useWaveformData.d.ts +1 -1
  10. package/dist/types/components/core/useWaveformLayout.d.ts +0 -1
  11. package/dist/types/components/core/useWaveformPresentation.d.ts +5 -0
  12. package/dist/types/components/core/waveformChartTypes.d.ts +9 -5
  13. package/dist/types/components/data/types.d.ts +1 -1
  14. package/dist/types/components/index.d.ts +1 -1
  15. package/dist/types/components/interaction/pointerCapture.d.ts +1 -0
  16. package/dist/types/components/interaction/viewportInteractionState.d.ts +39 -0
  17. package/dist/types/components/interaction/zoomConstraints.d.ts +14 -0
  18. package/dist/types/components/waveform.d.ts +1 -1
  19. package/dist/types/core/rendering.d.ts +1 -4
  20. package/dist/types/core/renderingStrategies.d.ts +22 -0
  21. package/dist/types/demo/types.d.ts +4 -2
  22. package/dist/types/index.d.ts +1 -1
  23. package/dist/types/types/chart.d.ts +7 -0
  24. package/dist/types/types/index.d.ts +1 -1
  25. package/package.json +5 -1
  26. package/src/App.vue +10 -4
  27. package/src/components/WaveformChart.vue +1 -0
  28. package/src/components/WaveformChartView.vue +15 -12
  29. package/src/components/core/constants.ts +3 -0
  30. package/src/components/core/useWaveformChartController.ts +7 -2
  31. package/src/components/core/useWaveformChartLifecycle.ts +47 -3
  32. package/src/components/core/useWaveformData.ts +6 -1
  33. package/src/components/core/useWaveformLayout.ts +0 -3
  34. package/src/components/core/useWaveformPresentation.ts +17 -2
  35. package/src/components/core/waveformChartTypes.ts +7 -3
  36. package/src/components/data/types.ts +1 -0
  37. package/src/components/index.ts +1 -0
  38. package/src/components/interaction/WaveformTooltip.test.ts +39 -0
  39. package/src/components/interaction/WaveformTooltip.vue +72 -22
  40. package/src/components/interaction/pointerCapture.ts +10 -0
  41. package/src/components/interaction/useWaveformHover.ts +1 -1
  42. package/src/components/interaction/useWaveformViewport.ts +86 -74
  43. package/src/components/interaction/useWaveformZoom.ts +46 -14
  44. package/src/components/interaction/viewportInteractionState.test.ts +101 -0
  45. package/src/components/interaction/viewportInteractionState.ts +89 -0
  46. package/src/components/interaction/zoomConstraints.ts +167 -0
  47. package/src/components/waveform.ts +1 -0
  48. package/src/components/waveformChartCases/sizingAndTitles.test.ts +59 -0
  49. package/src/components/waveformChartCases/viewportLifecycle.test.ts +132 -0
  50. package/src/components/waveformChartCases/zoomPointConstraints.test.ts +248 -0
  51. package/src/core/rendering.ts +11 -81
  52. package/src/core/renderingStrategies.test.ts +90 -0
  53. package/src/core/renderingStrategies.ts +131 -0
  54. package/src/data/simulatedWaveforms.ts +9 -10
  55. package/src/demo/DemoChartHost.vue +2 -2
  56. package/src/demo/DemoGridFrameControls.vue +29 -0
  57. package/src/demo/plotMarginControls.test.ts +29 -0
  58. package/src/demo/types.ts +4 -1
  59. package/src/index.ts +1 -0
  60. package/src/styles.css +1 -0
  61. package/src/types/chart.ts +8 -0
  62. package/src/types/index.ts +1 -0
  63. package/src/utils/formatters.test.ts +4 -3
  64. package/src/utils/formatters.ts +1 -2
  65. 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 = ref<ViewportSelectionState | null>(null)
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(configureZoom)
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(data: () => WaveformData, onDataChange: () => void) {
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 chartTopMargin = computed(() => margin.top)
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 innerHeight = computed(() => Math.max(0, drawingHeight.value - margin.top - margin.bottom))
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
- export interface ViewportSelectionState {
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' })
@@ -13,6 +13,7 @@ export type {
13
13
  WaveformAnnotationStyle,
14
14
  WaveformAnnotation,
15
15
  WaveformRenderingOptions,
16
+ WaveformPlotMargin,
16
17
  WaveformTitleTextStyle,
17
18
  WaveformTitleOptions,
18
19
  WaveformLegendPosition,
@@ -11,6 +11,7 @@ export type {
11
11
  WaveformAnnotationStyle,
12
12
  WaveformAnnotation,
13
13
  WaveformRenderingOptions,
14
+ WaveformPlotMargin,
14
15
  WaveformTitleTextStyle,
15
16
  WaveformTitleOptions,
16
17
  WaveformLegendPosition,
@@ -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 tooltipMaxWidth = 238
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 - tooltipMaxWidth
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 + tooltipMaxWidth <= props.containerWidth - containerPadding
46
- ? { left: `${rightPlacement}px` }
74
+ rightPlacement + tooltipPlacementWidth <= props.containerWidth - containerPadding
75
+ ? {
76
+ left: `${rightPlacement}px`,
77
+ maxWidth: `${Math.min(tooltipMaxWidth, rightAvailableWidth)}px`,
78
+ }
47
79
  : leftPlacement >= containerPadding
48
- ? { right: `${props.containerWidth - props.position.x + tooltipGap}px` }
49
- : { left: `${containerPadding}px` }
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(8, Math.min(props.position.y - 18, props.containerHeight - estimatedHeight - 8))}px`,
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
- <strong v-if="seriesPoint.name">{{ seriesPoint.name }}:</strong>
80
- <span class="waveform-tooltip__value">
81
- {{ formatTooltipNumber(seriesPoint.point.y)
82
- }}{{ seriesPoint.unit ? ` ${seriesPoint.unit}` : '' }}
83
- <small v-if="formatError(seriesPoint.point)">{{ formatError(seriesPoint.point) }}</small>
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
- min-width: 180px;
97
- max-width: 238px;
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) auto;
165
+ grid-template-columns: 8px minmax(0, 1fr);
120
166
  gap: 6px;
121
- align-items: center;
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
- white-space: nowrap;
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?.overlay === event.currentTarget) {
139
+ if (selection.value && !selection.value.independent) {
140
140
  updateViewportDrag(event)
141
141
  return
142
142
  }