waveform-analysis 0.1.38 → 0.1.39

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.
@@ -1,6 +1,7 @@
1
1
  import type { WaveformChartProps } from './core/waveformChartTypes';
2
2
  declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
3
3
  resetViewport: (trackIndex?: number) => void;
4
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
4
5
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
5
6
  "point-hover": (point: import("..").WaveformPoint | null) => any;
6
7
  "zoom-change": (domain: [number, number]) => any;
@@ -228,6 +228,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
228
228
  finishViewportDrag: (event: PointerEvent) => void;
229
229
  cancelViewportDrag: (event?: PointerEvent) => void;
230
230
  resetViewport: (trackIndex?: number) => void;
231
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
231
232
  requestViewportReset: (event: MouseEvent) => void;
232
233
  toggleSeriesVisibility: (seriesId: string) => void;
233
234
  changeDraftSeries: (seriesId: string) => void;
@@ -31,6 +31,9 @@ interface LifecycleContext {
31
31
  trackLayouts: ComputedRef<TrackLayout[]>;
32
32
  innerWidth: ComputedRef<number>;
33
33
  innerHeight: ComputedRef<number>;
34
+ sharedZoomDomain: ComputedRef<[number, number]>;
35
+ initialXDomain: ComputedRef<[number, number]>;
36
+ sharedTransform: ShallowRef<ZoomTransform>;
34
37
  activeInteractionMode: ComputedRef<string | undefined>;
35
38
  hiddenSeriesIdSet: ComputedRef<Set<string>>;
36
39
  internalHiddenSeriesIds: Ref<Set<string>>;
@@ -42,6 +42,7 @@ export declare function useWaveformViewport(context: ViewportContext): {
42
42
  finishViewportDrag: (event: PointerEvent) => void;
43
43
  cancelViewportDrag: (event?: PointerEvent) => void;
44
44
  resetViewport: (trackIndex?: number) => void;
45
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void;
45
46
  requestViewportReset: (event: MouseEvent) => void;
46
47
  };
47
48
  export {};
@@ -0,0 +1,21 @@
1
+ import { type ComputedRef, type Ref, type ShallowRef } from 'vue';
2
+ import type { ZoomTransform } from 'd3';
3
+ import type { TrackLayout } from '../core/types';
4
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes';
5
+ import type { AnnotationSeriesCandidate } from '../annotation';
6
+ interface ViewportDomainContext {
7
+ props: ResolvedWaveformChartProps;
8
+ trackLayouts: ComputedRef<TrackLayout[]>;
9
+ initialXDomain: ComputedRef<[number, number]>;
10
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number];
11
+ innerWidth: ComputedRef<number>;
12
+ sharedTransform: ShallowRef<ZoomTransform>;
13
+ independentTransforms: ShallowRef<ZoomTransform[]>;
14
+ cancelPendingZoom: () => void;
15
+ cancelViewportDrag: () => void;
16
+ clearHover: () => void;
17
+ editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>;
18
+ configureZoom: () => void;
19
+ }
20
+ export declare function createViewportDomainSetter(context: ViewportDomainContext): (domain: [number, number], trackIndex?: number) => void;
21
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.38",
3
+ "version": "0.1.39",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -36,6 +36,7 @@ const controller = useWaveformChartController(props as ResolvedWaveformChartProp
36
36
 
37
37
  defineExpose({
38
38
  resetViewport: controller.resetViewport,
39
+ setViewportDomain: controller.setViewportDomain,
39
40
  })
40
41
  </script>
41
42
 
@@ -259,6 +259,9 @@ export function useWaveformChartController(
259
259
  trackLayouts,
260
260
  innerWidth,
261
261
  innerHeight,
262
+ sharedZoomDomain,
263
+ initialXDomain,
264
+ sharedTransform,
262
265
  activeInteractionMode,
263
266
  hiddenSeriesIdSet,
264
267
  internalHiddenSeriesIds,
@@ -39,6 +39,9 @@ interface LifecycleContext {
39
39
  trackLayouts: ComputedRef<TrackLayout[]>
40
40
  innerWidth: ComputedRef<number>
41
41
  innerHeight: ComputedRef<number>
42
+ sharedZoomDomain: ComputedRef<[number, number]>
43
+ initialXDomain: ComputedRef<[number, number]>
44
+ sharedTransform: ShallowRef<ZoomTransform>
42
45
  activeInteractionMode: ComputedRef<string | undefined>
43
46
  hiddenSeriesIdSet: ComputedRef<Set<string>>
44
47
  internalHiddenSeriesIds: Ref<Set<string>>
@@ -93,6 +96,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
93
96
  trackLayouts,
94
97
  innerWidth,
95
98
  innerHeight,
99
+ sharedZoomDomain,
100
+ initialXDomain,
101
+ sharedTransform,
96
102
  activeInteractionMode,
97
103
  hiddenSeriesIdSet,
98
104
  internalHiddenSeriesIds,
@@ -144,25 +150,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
144
150
  emit('page-change', nextPage, pageCount.value)
145
151
  }
146
152
 
153
+ let pendingSharedXDomain: [number, number] | undefined
147
154
  let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
148
155
 
149
156
  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
- })
157
+ if (props.displayMode === 'independent') {
158
+ pendingSharedXDomain = undefined
159
+ pendingIndependentXDomains = trackLayouts.value.map((track) => {
160
+ const current = track.xScale.domain() as [number, number]
161
+ const boundary = resolveInitialTrackDomain(track)
162
+ return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? current : undefined
163
+ })
164
+ return
165
+ }
166
+ pendingIndependentXDomains = undefined
167
+ const current = sharedZoomDomain.value
168
+ const boundary = initialXDomain.value
169
+ pendingSharedXDomain =
170
+ current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
166
171
  }
167
172
 
168
173
  function handleDataReferenceChange() {
@@ -186,10 +191,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
186
191
  })
187
192
  independentTransforms.value = nextTransforms
188
193
  pendingIndependentXDomains = undefined
189
- void nextTick(configureZoom)
190
- return
194
+ } else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
195
+ const boundary = initialXDomain.value
196
+ const groups = trackLayouts.value
197
+ .filter((track) => track.hasVisibleSeries)
198
+ .map((track) => track.seriesList)
199
+ const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
200
+ sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
201
+ pendingSharedXDomain = undefined
191
202
  }
192
- pendingIndependentXDomains = undefined
193
203
  configureZoom()
194
204
  })
195
205
  }
@@ -11,6 +11,7 @@ import type {
11
11
  import type { AnnotationSeriesCandidate } from '../annotation'
12
12
  import { tryReleasePointerCapture } from './pointerCapture'
13
13
  import { transitionViewportInteraction } from './viewportInteractionState'
14
+ import { createViewportDomainSetter } from './viewportDomain'
14
15
  import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
15
16
  interface ViewportContext {
16
17
  props: ResolvedWaveformChartProps
@@ -344,6 +345,20 @@ export function useWaveformViewport(context: ViewportContext) {
344
345
  editorSeriesOptions.value = []
345
346
  void nextTick(configureZoom)
346
347
  }
348
+ const setViewportDomain = createViewportDomainSetter({
349
+ props,
350
+ trackLayouts,
351
+ initialXDomain,
352
+ resolveInitialTrackDomain,
353
+ innerWidth,
354
+ sharedTransform,
355
+ independentTransforms,
356
+ cancelPendingZoom,
357
+ cancelViewportDrag,
358
+ clearHover,
359
+ editorSeriesOptions,
360
+ configureZoom,
361
+ })
347
362
  const requestViewportReset = (event: MouseEvent) => {
348
363
  if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
349
364
  if (props.displayMode === 'independent') {
@@ -372,6 +387,7 @@ export function useWaveformViewport(context: ViewportContext) {
372
387
  finishViewportDrag,
373
388
  cancelViewportDrag,
374
389
  resetViewport,
390
+ setViewportDomain,
375
391
  requestViewportReset,
376
392
  }
377
393
  }
@@ -0,0 +1,60 @@
1
+ import { nextTick, type ComputedRef, type Ref, type ShallowRef } from 'vue'
2
+ import type { ZoomTransform } from 'd3'
3
+
4
+ import type { TrackLayout } from '../core/types'
5
+ import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
6
+ import type { AnnotationSeriesCandidate } from '../annotation'
7
+ import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
8
+
9
+ interface ViewportDomainContext {
10
+ props: ResolvedWaveformChartProps
11
+ trackLayouts: ComputedRef<TrackLayout[]>
12
+ initialXDomain: ComputedRef<[number, number]>
13
+ resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
14
+ innerWidth: ComputedRef<number>
15
+ sharedTransform: ShallowRef<ZoomTransform>
16
+ independentTransforms: ShallowRef<ZoomTransform[]>
17
+ cancelPendingZoom: () => void
18
+ cancelViewportDrag: () => void
19
+ clearHover: () => void
20
+ editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
21
+ configureZoom: () => void
22
+ }
23
+
24
+ export function createViewportDomainSetter(context: ViewportDomainContext) {
25
+ return (domain: [number, number], trackIndex?: number) => {
26
+ if (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) || domain[0] === domain[1])
27
+ return
28
+ context.cancelPendingZoom()
29
+ context.cancelViewportDrag()
30
+ if (context.props.displayMode === 'independent') {
31
+ const indexes =
32
+ trackIndex === undefined
33
+ ? context.trackLayouts.value.map((track) => track.index)
34
+ : [trackIndex]
35
+ const nextTransforms = [...context.independentTransforms.value]
36
+ indexes.forEach((index) => {
37
+ const track = context.trackLayouts.value.find((item) => item.index === index)
38
+ if (!track) return
39
+ const boundary = context.resolveInitialTrackDomain(track)
40
+ const constrained = constrainZoomDomain(domain, boundary, [track.seriesList], context.props)
41
+ nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
42
+ })
43
+ context.independentTransforms.value = nextTransforms
44
+ } else {
45
+ const boundary = context.initialXDomain.value
46
+ const groups = context.trackLayouts.value
47
+ .filter((track) => track.hasVisibleSeries)
48
+ .map((track) => track.seriesList)
49
+ const constrained = constrainZoomDomain(domain, boundary, groups, context.props)
50
+ context.sharedTransform.value = transformForDomain(
51
+ constrained,
52
+ boundary,
53
+ context.innerWidth.value,
54
+ )
55
+ }
56
+ context.clearHover()
57
+ context.editorSeriesOptions.value = []
58
+ void nextTick(context.configureZoom)
59
+ }
60
+ }
@@ -119,6 +119,108 @@ describe('WaveformChart viewport reset', () => {
119
119
  expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
120
120
  })
121
121
 
122
+ it('keeps a controlled shared viewport when the data reference changes', async () => {
123
+ const wrapper = await mountSizedChart(
124
+ {
125
+ kind: 'points',
126
+ points: [
127
+ { x: 0, y: 0 },
128
+ { x: 1, y: 1 },
129
+ ],
130
+ },
131
+ { displayMode: 'separated' },
132
+ )
133
+ const chart = wrapper.vm as unknown as {
134
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
135
+ }
136
+ chart.setViewportDomain([0.25, 0.75])
137
+ await flushPromises()
138
+
139
+ const startBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
140
+ const endBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
141
+ expect(startBefore).toBeGreaterThan(0)
142
+ expect(endBefore).toBeLessThan(1000)
143
+
144
+ await wrapper.setProps({
145
+ data: {
146
+ kind: 'points',
147
+ points: [
148
+ { x: 0, y: 10 },
149
+ { x: 1, y: 11 },
150
+ ],
151
+ },
152
+ })
153
+ await flushPromises()
154
+
155
+ expect(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())).toBeCloseTo(
156
+ startBefore,
157
+ 10,
158
+ )
159
+ expect(Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())).toBeCloseTo(
160
+ endBefore,
161
+ 10,
162
+ )
163
+ })
164
+
165
+ it('keeps independently controlled track viewports when the data reference changes', async () => {
166
+ const wrapper = await mountSizedChart(gridSeries(2), {
167
+ displayMode: 'independent',
168
+ grid: { rowCount: 1, columnCount: 2 },
169
+ })
170
+ const chart = wrapper.vm as unknown as {
171
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
172
+ }
173
+ chart.setViewportDomain([0.2, 0.8], 0)
174
+ chart.setViewportDomain([0.1, 0.9], 1)
175
+ await flushPromises()
176
+
177
+ const endpoints = () =>
178
+ wrapper.findAll('.waveform-chart__track').map((track) => ({
179
+ start: track.get('.waveform-chart__axis-endpoint--start').text(),
180
+ end: track.get('.waveform-chart__axis-endpoint--end').text(),
181
+ }))
182
+ const endpointsBefore = endpoints()
183
+ expect(Number(endpointsBefore[0].start)).toBeGreaterThan(0)
184
+ expect(Number(endpointsBefore[0].end)).toBeLessThan(1000)
185
+ expect(Number(endpointsBefore[1].start)).toBeGreaterThan(0)
186
+ expect(Number(endpointsBefore[1].end)).toBeLessThan(1000)
187
+
188
+ await wrapper.setProps({ data: gridSeries(2) })
189
+ await flushPromises()
190
+
191
+ const endpointsAfter = endpoints()
192
+ endpointsAfter.forEach((endpoint, index) => {
193
+ expect(Number(endpoint.start)).toBeCloseTo(Number(endpointsBefore[index].start), 10)
194
+ expect(Number(endpoint.end)).toBeCloseTo(Number(endpointsBefore[index].end), 10)
195
+ })
196
+ })
197
+
198
+ it('ignores invalid domains and applies viewport constraints', async () => {
199
+ const wrapper = await mountSizedChart(
200
+ {
201
+ kind: 'points',
202
+ points: [
203
+ { x: 0, y: 0 },
204
+ { x: 1, y: 1 },
205
+ ],
206
+ },
207
+ { displayMode: 'separated', minZoomSpan: 0.2 },
208
+ )
209
+ const chart = wrapper.vm as unknown as {
210
+ setViewportDomain: (domain: [number, number], trackIndex?: number) => void
211
+ }
212
+ chart.setViewportDomain([Number.NaN, 0.5])
213
+ await flushPromises()
214
+ expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
215
+ expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
216
+
217
+ chart.setViewportDomain([0.49, 0.51])
218
+ await flushPromises()
219
+ const start = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
220
+ const end = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
221
+ expect(end - start).toBeGreaterThanOrEqual(200)
222
+ })
223
+
122
224
  it('resets shared and independent explicit domains to their included nice bounds', async () => {
123
225
  const shared = await mountSizedChart(
124
226
  {