waveform-analysis 0.1.59 → 0.1.60

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.
@@ -3,6 +3,12 @@ import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
3
3
  import { type WaveformYDomain } from './yDomain';
4
4
  export { MAX_MULTI_Y_AXIS_COUNT } from './constants';
5
5
  export declare function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number;
6
+ export interface ResolvedYAxisTicks {
7
+ domain: [number, number];
8
+ values: number[];
9
+ }
10
+ export declare function resolveYAxisTicks(domain: [number, number], tickCount?: number, nice?: boolean): ResolvedYAxisTicks;
11
+ export declare function formatYAxisTickLabel(value: number, domain: [number, number], tickValues: readonly number[], unit?: string): string;
6
12
  export interface YAxisSeriesGroup {
7
13
  index: number;
8
14
  side: 'left' | 'right';
@@ -19,7 +25,8 @@ export interface YAxisSlot {
19
25
  }
20
26
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
21
27
  export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
22
- export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
28
+ export declare function resolveRenderedYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, viewportYDomains?: Record<string, [number, number]>): YAxisSeriesGroup[];
29
+ export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number, includeWithoutLastTick?: boolean): {
23
30
  tickTextWidth: number;
24
31
  };
25
32
  export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number, nice?: boolean): number;
@@ -31,7 +38,7 @@ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayM
31
38
  * Reserves stable Y-axis positions for every visible track on the current page.
32
39
  * Slots are ordered from the plot edge outward for each side.
33
40
  */
34
- export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
41
+ export declare function buildYAxisSlots(tracks: readonly DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean, includeWithoutLastTick?: boolean, viewportYDomains?: Record<string, [number, number]>): {
35
42
  slots: YAxisSlot[];
36
43
  clearance: {
37
44
  left: number;
@@ -0,0 +1,10 @@
1
+ import type { WaveformOverlayMode } from '../data/types';
2
+ import type { DisplayTrack } from './types';
3
+ import type { WaveformYDomain } from './yDomain';
4
+ export interface YAxisLayoutMetrics {
5
+ tickClearance: number;
6
+ fullClearance: number;
7
+ labelCenterX: number;
8
+ }
9
+ export declare function resolveViewportYDomains(displayMode: 'independent' | 'separated' | 'compact', chartTracks: DisplayTrack[], sharedYDomains: Record<string, [number, number]>, independentYDomains: Record<number, [number, number]>): Record<string, [number, number]>;
10
+ export declare function resolveYAxisLayoutMetrics(chartTracks: DisplayTrack[], overlayMode: WaveformOverlayMode, yDomain: WaveformYDomain | undefined, yDomains: Record<string, WaveformYDomain> | undefined, viewportYDomains: Record<string, [number, number]>, nice: boolean, tickCount: number, compact: boolean): YAxisLayoutMetrics;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.59",
3
+ "version": "0.1.60",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -4,11 +4,14 @@ import { describe, expect, it } from 'vitest'
4
4
  import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
5
  import type { DisplaySeries, DisplayTrack } from './types'
6
6
  import {
7
+ axisTextMetrics,
7
8
  buildTrackLayouts,
8
9
  buildYAxisSeriesGroups,
10
+ formatYAxisTickLabel,
9
11
  findClosestTrackAtPointer,
10
12
  MAX_MULTI_Y_AXIS_COUNT,
11
13
  measureYAxisGroupClearance,
14
+ resolveYAxisTicks,
12
15
  resolveYAxisTickCount,
13
16
  } from './layout'
14
17
 
@@ -143,6 +146,27 @@ describe('multi-value Y-axis grouping', () => {
143
146
  expect(resolveYAxisTickCount(220)).toBe(5)
144
147
  })
145
148
 
149
+ it('measures the same final endpoint label that Y-axis rendering uses', () => {
150
+ const ticks = resolveYAxisTicks([-0.7434, -0.3434], 5, false)
151
+ const labels = ticks.values.map((value) =>
152
+ formatYAxisTickLabel(value, ticks.domain, ticks.values, 'V'),
153
+ )
154
+
155
+ expect(labels).toEqual(['-0.7434', '-0.6434', '-0.5434', '-0.4434', '(V) -0.3434'])
156
+ expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(77)
157
+ })
158
+
159
+ it('includes the compact-axis unit label after its top tick is hidden', () => {
160
+ const ticks = resolveYAxisTicks([0.12345, 100], 5, false)
161
+ const compactTicks = ticks.values.slice(0, -1)
162
+
163
+ expect(formatYAxisTickLabel(compactTicks.at(-1)!, ticks.domain, compactTicks, 'V')).toBe(
164
+ '(V) 75.031',
165
+ )
166
+ expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5).tickTextWidth).toBe(49)
167
+ expect(axisTextMetrics(ticks.domain, false, undefined, 'V', 5, true).tickTextWidth).toBe(70)
168
+ })
169
+
146
170
  it('keeps every overlaid series on one axis in single-axis mode', () => {
147
171
  const groups = buildYAxisSeriesGroups(
148
172
  track([series('a', 0, 1), series('b', 10, 20)]),
@@ -27,6 +27,47 @@ export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number)
27
27
  return 5
28
28
  }
29
29
 
30
+ export interface ResolvedYAxisTicks {
31
+ domain: [number, number]
32
+ values: number[]
33
+ }
34
+
35
+ export function resolveYAxisTicks(
36
+ domain: [number, number],
37
+ tickCount = 5,
38
+ nice = true,
39
+ ): ResolvedYAxisTicks {
40
+ const effectiveTickCount =
41
+ typeof tickCount === 'number' && Number.isFinite(tickCount)
42
+ ? Math.max(2, Math.floor(tickCount))
43
+ : 5
44
+ const scale = scaleLinear(domain, [1, 0])
45
+ if (nice) scale.nice(Math.max(1, effectiveTickCount - 1))
46
+ const resolvedDomain = scale.domain() as [number, number]
47
+ const [axisStart, axisEnd] = resolvedDomain
48
+ return {
49
+ domain: resolvedDomain,
50
+ values: Array.from(
51
+ { length: effectiveTickCount },
52
+ (_, index) => axisStart + ((axisEnd - axisStart) * index) / (effectiveTickCount - 1),
53
+ ),
54
+ }
55
+ }
56
+
57
+ export function formatYAxisTickLabel(
58
+ value: number,
59
+ domain: [number, number],
60
+ tickValues: readonly number[],
61
+ unit?: string,
62
+ ): string {
63
+ return formatScientificAxisLabel(value, {
64
+ axisMin: domain[0],
65
+ axisMax: domain[1],
66
+ topTickValue: Math.max(...tickValues),
67
+ unit,
68
+ })
69
+ }
70
+
30
71
  export interface YAxisSeriesGroup {
31
72
  index: number
32
73
  side: 'left' | 'right'
@@ -114,26 +155,38 @@ export function resolveYAxisSeriesGroups(
114
155
  })
115
156
  }
116
157
 
158
+ export function resolveRenderedYAxisSeriesGroups(
159
+ track: DisplayTrack,
160
+ overlayMode: WaveformOverlayMode,
161
+ yDomain?: WaveformYDomain,
162
+ yDomains?: Record<string, WaveformYDomain>,
163
+ viewportYDomains?: Record<string, [number, number]>,
164
+ ): YAxisSeriesGroup[] {
165
+ const viewportYDomain = viewportYDomains?.[track.id]
166
+ return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).map((group) =>
167
+ !group.fixed && viewportYDomain ? { ...group, domain: viewportYDomain } : group,
168
+ )
169
+ }
170
+
117
171
  export function axisTextMetrics(
118
172
  domain: [number, number],
119
173
  nice = true,
120
174
  tickValues?: number[],
121
175
  unit?: string,
122
- tickCount = 10,
176
+ tickCount = 5,
177
+ includeWithoutLastTick = false,
123
178
  ): { tickTextWidth: number } {
124
- const effectiveTickCount =
125
- typeof tickCount === 'number' && Number.isFinite(tickCount)
126
- ? Math.max(2, Math.floor(tickCount))
127
- : 10
128
- const scale = scaleLinear(domain, [1, 0])
129
- if (nice) scale.nice(effectiveTickCount)
130
- const [axisMin, axisMax] = scale.domain()
131
- const values = tickValues ?? scale.ticks(effectiveTickCount)
132
- const topTickValue = Math.max(...values)
179
+ const resolvedTicks = tickValues
180
+ ? { domain, values: tickValues }
181
+ : resolveYAxisTicks(domain, tickCount, nice)
182
+ const tickValueSets = [resolvedTicks.values]
183
+ if (includeWithoutLastTick && resolvedTicks.values.length > 1) {
184
+ tickValueSets.push(resolvedTicks.values.slice(0, -1))
185
+ }
133
186
  const maximumTickCharacters = Math.max(
134
187
  1,
135
- ...values.map(
136
- (value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue, unit }).length,
188
+ ...tickValueSets.flatMap((values) =>
189
+ values.map((value) => formatYAxisTickLabel(value, resolvedTicks.domain, values, unit).length),
137
190
  ),
138
191
  )
139
192
  return {
@@ -200,11 +253,19 @@ export function buildYAxisSlots(
200
253
  yDomains?: Record<string, WaveformYDomain>,
201
254
  tickCount?: number,
202
255
  nice = true,
256
+ includeWithoutLastTick = false,
257
+ viewportYDomains?: Record<string, [number, number]>,
203
258
  ): { slots: YAxisSlot[]; clearance: { left: number; right: number } } {
204
259
  const widths = new Map<string, number>()
205
260
  tracks.forEach((track) => {
206
261
  const sideIndexes = { left: 0, right: 0 }
207
- resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).forEach((group) => {
262
+ resolveRenderedYAxisSeriesGroups(
263
+ track,
264
+ overlayMode,
265
+ yDomain,
266
+ yDomains,
267
+ viewportYDomains,
268
+ ).forEach((group) => {
208
269
  const sideIndex = sideIndexes[group.side]++
209
270
  const key = `${group.side}:${sideIndex}`
210
271
  const width = axisTextMetrics(
@@ -213,6 +274,7 @@ export function buildYAxisSlots(
213
274
  undefined,
214
275
  group.seriesList[0]?.unit,
215
276
  tickCount,
277
+ includeWithoutLastTick,
216
278
  ).tickTextWidth
217
279
  widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, width))
218
280
  })
@@ -27,7 +27,8 @@ import {
27
27
  } from './grid'
28
28
  import {
29
29
  axisTextMetrics,
30
- resolveYAxisSeriesGroups,
30
+ resolveRenderedYAxisSeriesGroups,
31
+ resolveYAxisTicks,
31
32
  resolveYAxisTickCount,
32
33
  type YAxisSlot,
33
34
  } from './layout'
@@ -115,28 +116,21 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
115
116
  ? (options.independentTransforms[index] ?? zoomIdentity)
116
117
  : zoomIdentity
117
118
  const xScale = transform.rescaleX(baseXScale)
118
- const configuredYDomain = options.yDomains?.[displayTrack.id]
119
- const yAxisGroups = resolveYAxisSeriesGroups(
119
+ const yAxisGroups = resolveRenderedYAxisSeriesGroups(
120
120
  displayTrack,
121
121
  options.overlayMode,
122
122
  options.fixedYDomain,
123
123
  options.fixedYDomains,
124
- ).map((group) =>
125
- !group.fixed && configuredYDomain ? { ...group, domain: configuredYDomain } : group,
124
+ options.yDomains,
126
125
  )
127
126
  const sideIndexes = { left: 0, right: 0 }
128
127
  const sideOffsets = { left: 0, right: 0 }
129
128
  const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
130
129
  const sideIndex = sideIndexes[group.side]++
131
130
  const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
132
- const niceCount = Math.max(1, tickCount - 1)
133
- const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
134
- if (options.yAxisNice !== false) scale.nice(niceCount)
135
- const [axisStart, axisEnd] = scale.domain()
136
- const majorTicks = Array.from(
137
- { length: tickCount },
138
- (_, index) => axisStart + ((axisEnd - axisStart) * index) / (tickCount - 1),
139
- )
131
+ const resolvedTicks = resolveYAxisTicks(group.domain, tickCount, options.yAxisNice !== false)
132
+ const scale = scaleLinear(resolvedTicks.domain, [cell.plotHeight, 0])
133
+ const majorTicks = resolvedTicks.values
140
134
  const showAxisEnd = options.displayMode !== 'compact' || cell.row === 0
141
135
  const visibleMajorTicks = showAxisEnd ? majorTicks : majorTicks.slice(0, -1)
142
136
  const tickValues = visibleMajorTicks
@@ -7,16 +7,7 @@ import {
7
7
  type AnnotationSeriesInfo,
8
8
  type AnnotationTrackLayout,
9
9
  } from '../annotation'
10
- import {
11
- channelColors,
12
- margin,
13
- MINIMUM_PLOT_WIDTH,
14
- Y_AXIS_CHARACTER_WIDTH,
15
- Y_AXIS_LABEL_BAND_WIDTH,
16
- Y_AXIS_LABEL_GAP,
17
- Y_AXIS_OUTER_PADDING,
18
- Y_AXIS_TICK_PADDING,
19
- } from './constants'
10
+ import { channelColors, margin, MINIMUM_PLOT_WIDTH } from './constants'
20
11
  import {
21
12
  getGridGap,
22
13
  getPageCount,
@@ -24,17 +15,13 @@ import {
24
15
  paginateSeries,
25
16
  resolveGridCellGeometry,
26
17
  } from './grid'
27
- import {
28
- buildTrackLayouts,
29
- axisTextMetrics,
30
- buildYAxisSlots,
31
- resolveYAxisSeriesGroups,
32
- } from './layout'
18
+ import { buildTrackLayouts, buildYAxisSlots, resolveYAxisTickCount } from './layout'
33
19
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
34
20
  import { createFrameNumberResolver, resolvePageableTracks } from './trackPagination'
35
21
  import type { PreparedWaveformSeries } from './useWaveformData'
36
22
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
37
23
  import { applyXDomainStrategy } from './xDomain'
24
+ import { resolveYAxisLayoutMetrics, resolveViewportYDomains } from './yAxisLayoutMetrics'
38
25
  import type { useWaveformAnnotationInteraction } from '../annotation'
39
26
  interface LayoutContext {
40
27
  props: ResolvedWaveformChartProps
@@ -121,38 +108,29 @@ export function useWaveformLayout(context: LayoutContext) {
121
108
  ? tracksWithSeries
122
109
  : pagedTracks.value
123
110
  })
124
- const yAxisMetrics = computed(() => {
125
- const axisText = chartTracks.value
126
- .filter((track) => track.visibleSeries.length > 0)
127
- .flatMap((track) =>
128
- resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
129
- )
130
- .map(
131
- (group) =>
132
- axisTextMetrics(
133
- group.domain,
134
- props.axes?.y?.nice !== false,
135
- undefined,
136
- group.seriesList[0]?.unit,
137
- props.axes?.y?.splitNumber,
138
- ).tickTextWidth,
139
- )
140
- const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
141
- const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
142
- const labelCenterX = -(
143
- Y_AXIS_TICK_PADDING +
144
- tickTextWidth +
145
- Y_AXIS_LABEL_GAP +
146
- Y_AXIS_LABEL_BAND_WIDTH / 2
147
- )
148
- const fullClearance =
149
- tickTextWidth +
150
- Y_AXIS_TICK_PADDING +
151
- Y_AXIS_LABEL_GAP +
152
- Y_AXIS_LABEL_BAND_WIDTH +
153
- Y_AXIS_OUTER_PADDING
154
- return { tickClearance, fullClearance, labelCenterX }
155
- })
111
+ const yAxisTickCount = computed(() =>
112
+ resolveYAxisTickCount(innerHeight.value, props.axes?.y?.splitNumber),
113
+ )
114
+ const viewportYDomains = computed(() =>
115
+ resolveViewportYDomains(
116
+ props.displayMode,
117
+ chartTracks.value,
118
+ sharedYDomains.value,
119
+ independentYDomains.value,
120
+ ),
121
+ )
122
+ const yAxisMetrics = computed(() =>
123
+ resolveYAxisLayoutMetrics(
124
+ chartTracks.value,
125
+ props.overlayMode,
126
+ props.yDomain,
127
+ props.yDomains,
128
+ viewportYDomains.value,
129
+ props.axes?.y?.nice !== false,
130
+ yAxisTickCount.value,
131
+ props.displayMode === 'compact',
132
+ ),
133
+ )
156
134
  const hasYAxisLabels = computed(() =>
157
135
  chartTracks.value.some(
158
136
  (track) =>
@@ -179,8 +157,10 @@ export function useWaveformLayout(context: LayoutContext) {
179
157
  props.overlayMode,
180
158
  props.yDomain,
181
159
  props.yDomains,
182
- props.axes?.y?.splitNumber,
160
+ yAxisTickCount.value,
183
161
  props.axes?.y?.nice !== false,
162
+ props.displayMode === 'compact',
163
+ viewportYDomains.value,
184
164
  ),
185
165
  )
186
166
  const resolvedChartLeftMargin = computed(() =>
@@ -302,15 +282,7 @@ export function useWaveformLayout(context: LayoutContext) {
302
282
  xDomainStrategy: props.xDomainStrategy,
303
283
  fixedYDomain: props.yDomain,
304
284
  fixedYDomains: props.yDomains,
305
- yDomains:
306
- props.displayMode === 'independent'
307
- ? Object.fromEntries(
308
- chartTracks.value.flatMap((track, index) => {
309
- const domain = independentYDomains.value[index]
310
- return domain ? [[track.id, domain]] : []
311
- }),
312
- )
313
- : sharedYDomains.value,
285
+ yDomains: viewportYDomains.value,
314
286
  timeUnit: props.timeUnit,
315
287
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
316
288
  yAxisSplitNumber: props.axes?.y?.splitNumber,
@@ -0,0 +1,75 @@
1
+ import type { WaveformOverlayMode } from '../data/types'
2
+ import {
3
+ Y_AXIS_CHARACTER_WIDTH,
4
+ Y_AXIS_LABEL_BAND_WIDTH,
5
+ Y_AXIS_LABEL_GAP,
6
+ Y_AXIS_OUTER_PADDING,
7
+ Y_AXIS_TICK_PADDING,
8
+ } from './constants'
9
+ import { axisTextMetrics, resolveRenderedYAxisSeriesGroups } from './layout'
10
+ import type { DisplayTrack } from './types'
11
+ import type { WaveformYDomain } from './yDomain'
12
+
13
+ export interface YAxisLayoutMetrics {
14
+ tickClearance: number
15
+ fullClearance: number
16
+ labelCenterX: number
17
+ }
18
+
19
+ export function resolveViewportYDomains(
20
+ displayMode: 'independent' | 'separated' | 'compact',
21
+ chartTracks: DisplayTrack[],
22
+ sharedYDomains: Record<string, [number, number]>,
23
+ independentYDomains: Record<number, [number, number]>,
24
+ ): Record<string, [number, number]> {
25
+ if (displayMode !== 'independent') return sharedYDomains
26
+ return Object.fromEntries(
27
+ chartTracks.flatMap((track, index) => {
28
+ const domain = independentYDomains[index]
29
+ return domain ? [[track.id, domain]] : []
30
+ }),
31
+ )
32
+ }
33
+
34
+ export function resolveYAxisLayoutMetrics(
35
+ chartTracks: DisplayTrack[],
36
+ overlayMode: WaveformOverlayMode,
37
+ yDomain: WaveformYDomain | undefined,
38
+ yDomains: Record<string, WaveformYDomain> | undefined,
39
+ viewportYDomains: Record<string, [number, number]>,
40
+ nice: boolean,
41
+ tickCount: number,
42
+ compact: boolean,
43
+ ): YAxisLayoutMetrics {
44
+ const axisText = chartTracks
45
+ .filter((track) => track.visibleSeries.length > 0)
46
+ .flatMap((track) =>
47
+ resolveRenderedYAxisSeriesGroups(track, overlayMode, yDomain, yDomains, viewportYDomains),
48
+ )
49
+ .map(
50
+ (group) =>
51
+ axisTextMetrics(
52
+ group.domain,
53
+ nice,
54
+ undefined,
55
+ group.seriesList[0]?.unit,
56
+ tickCount,
57
+ compact,
58
+ ).tickTextWidth,
59
+ )
60
+ const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
61
+ const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
62
+ const labelCenterX = -(
63
+ Y_AXIS_TICK_PADDING +
64
+ tickTextWidth +
65
+ Y_AXIS_LABEL_GAP +
66
+ Y_AXIS_LABEL_BAND_WIDTH / 2
67
+ )
68
+ const fullClearance =
69
+ tickTextWidth +
70
+ Y_AXIS_TICK_PADDING +
71
+ Y_AXIS_LABEL_GAP +
72
+ Y_AXIS_LABEL_BAND_WIDTH +
73
+ Y_AXIS_OUTER_PADDING
74
+ return { tickClearance, fullClearance, labelCenterX }
75
+ }
@@ -2,7 +2,7 @@ import { zoomIdentity } from 'd3'
2
2
  import { describe, expect, it } from 'vitest'
3
3
 
4
4
  import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
- import { buildTrackLayouts, buildYAxisSlots } from './layout'
5
+ import { buildTrackLayouts, buildYAxisSlots, resolveRenderedYAxisSeriesGroups } from './layout'
6
6
  import type { DisplaySeries, DisplayTrack } from './types'
7
7
  import { Y_AXIS_RIGHT_LABEL_OFFSET } from './yAxisConstants'
8
8
 
@@ -126,4 +126,40 @@ describe('Y-axis slots', () => {
126
126
  expect(fourAxisSlots.slots[1]!.axisOffset).toBeLessThan(0)
127
127
  expect(fourAxisSlots.slots[3]!.axisOffset).toBeGreaterThan(0)
128
128
  })
129
+
130
+ it('uses dynamic Y domains when measuring multi-axis slots', () => {
131
+ const negativeVoltage = series('negative-voltage', 0, 1)
132
+ negativeVoltage.unit = 'V'
133
+ const longUnit = series('long-unit', 0, 1)
134
+ longUnit.unit = 'volt-amperes-per-phase'
135
+ const sourceTrack = track([negativeVoltage, longUnit])
136
+ const viewportYDomains = { [sourceTrack.id]: [-0.7434, -0.3434] as [number, number] }
137
+
138
+ expect(
139
+ resolveRenderedYAxisSeriesGroups(
140
+ sourceTrack,
141
+ 'multi-axis',
142
+ undefined,
143
+ undefined,
144
+ viewportYDomains,
145
+ ).map((group) => group.domain),
146
+ ).toEqual([
147
+ [-0.7434, -0.3434],
148
+ [-0.7434, -0.3434],
149
+ ])
150
+
151
+ const slots = buildYAxisSlots(
152
+ [sourceTrack],
153
+ 'multi-axis',
154
+ undefined,
155
+ undefined,
156
+ 5,
157
+ false,
158
+ false,
159
+ viewportYDomains,
160
+ )
161
+
162
+ expect(slots.clearance.left).toBe(100)
163
+ expect(slots.clearance.right).toBeGreaterThan(180)
164
+ })
129
165
  })
@@ -3,7 +3,8 @@ import { axisBottom, axisLeft, axisRight, select } from 'd3'
3
3
  import { nextTick, onMounted, ref, watch } from 'vue'
4
4
 
5
5
  import type { WaveformAxesOptions } from '../../types'
6
- import { formatScientificAxisLabel, formatXAxisLabel } from '../../utils'
6
+ import { formatXAxisLabel } from '../../utils'
7
+ import { formatYAxisTickLabel } from '../core/layout'
7
8
  import type { DisplaySeries, TrackLayout, WaveformYAxisLayout } from '../core/types'
8
9
  import { Y_AXIS_LABEL_BAND_WIDTH } from '../core/yAxisConstants'
9
10
 
@@ -43,12 +44,15 @@ function renderAxes() {
43
44
  props.track.yAxes.forEach((axis, index) => {
44
45
  const element = yAxisElements.value[index]
45
46
  if (!element) return
46
- const [axisMin, axisMax] = axis.scale.domain()
47
- const topTickValue = Math.max(...axis.tickValues)
48
47
  const unit = axis.seriesList[0]?.unit
49
48
  const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
50
49
  .tickFormat((value) =>
51
- formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue, unit }),
50
+ formatYAxisTickLabel(
51
+ Number(value),
52
+ axis.scale.domain() as [number, number],
53
+ axis.tickValues,
54
+ unit,
55
+ ),
52
56
  )
53
57
  .tickSize(-4)
54
58
  .tickPadding(7)
@@ -6,26 +6,6 @@ import { resizeObservers } from '../../test/setup'
6
6
  import { gridSeries, mountSizedChart } from '../../test/waveformChart'
7
7
 
8
8
  describe('WaveformChart', () => {
9
- it('applies the configured Y-axis split number', async () => {
10
- const wrapper = await mountSizedChart(
11
- {
12
- kind: 'points',
13
- points: [
14
- { x: 0, y: 3 },
15
- { x: 1, y: 97 },
16
- ],
17
- },
18
- { axes: { y: { splitNumber: 5 } } },
19
- )
20
-
21
- expect(
22
- wrapper
23
- .get('.waveform-chart__axis--y')
24
- .findAll('.tick text')
25
- .map((tick) => tick.text()),
26
- ).toEqual(['0', '25', '50', '75', '100'])
27
- })
28
-
29
9
  it('expands the Y-axis label gutter for signed values and long exponents', async () => {
30
10
  const wrapper = await mountSizedChart(
31
11
  {
@@ -52,10 +32,13 @@ describe('WaveformChart', () => {
52
32
  const labelBackgroundX = Number(
53
33
  tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
54
34
  )
35
+ const longestTickLabel = Math.max(
36
+ ...tracks[0].findAll('.waveform-chart__axis--y .tick text').map((tick) => tick.text().length),
37
+ )
55
38
 
56
- expect(labelX).toBe(-62)
39
+ expect(Math.abs(labelX)).toBeGreaterThanOrEqual(7 + longestTickLabel * 7 + 6)
57
40
  expect(labelBackgroundX).toBe(labelX - 6)
58
- expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(80)
41
+ expect(Number(wrapper.attributes('data-chart-left-margin'))).toBeGreaterThanOrEqual(80)
59
42
  expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(72)
60
43
  })
61
44
 
@@ -118,12 +101,28 @@ describe('WaveformChart', () => {
118
101
  { grid: { rowCount: 1, columnCount: 1 } },
119
102
  )
120
103
  const initialMargin = wrapper.attributes('data-chart-left-margin')
121
- expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-34')
104
+ const initialTrack = wrapper.get('.waveform-chart__track')
105
+ const initialLongestTickLabel = Math.max(
106
+ ...initialTrack
107
+ .findAll('.waveform-chart__axis--y .tick text')
108
+ .map((tick) => tick.text().length),
109
+ )
110
+ expect(initialTrack.attributes('data-y-axis-label-x')).toBe(
111
+ String(-(7 + initialLongestTickLabel * 7 + 6)),
112
+ )
122
113
 
123
114
  await wrapper.get('.ant-pagination-next button').trigger('click')
124
115
 
125
116
  expect(wrapper.attributes('data-chart-left-margin')).toBe(initialMargin)
126
- expect(wrapper.get('.waveform-chart__track').attributes('data-y-axis-label-x')).toBe('-62')
117
+ const pagedTrack = wrapper.get('.waveform-chart__track')
118
+ const pagedLongestTickLabel = Math.max(
119
+ ...pagedTrack
120
+ .findAll('.waveform-chart__axis--y .tick text')
121
+ .map((tick) => tick.text().length),
122
+ )
123
+ expect(pagedTrack.attributes('data-y-axis-label-x')).toBe(
124
+ String(-(7 + pagedLongestTickLabel * 7 + 6)),
125
+ )
127
126
  })
128
127
 
129
128
  it('hides only secondary-column Y-axis labels when the grid is too narrow', async () => {