waveform-analysis 0.1.40 → 0.1.41

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.
@@ -2,6 +2,7 @@ import type { WaveformOverlayMode } from '../../types';
2
2
  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
+ export declare function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number;
5
6
  export interface YAxisSeriesGroup {
6
7
  index: number;
7
8
  side: 'left' | 'right';
@@ -11,11 +12,11 @@ export interface YAxisSeriesGroup {
11
12
  }
12
13
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
13
14
  export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
14
- export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string): {
15
+ export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
15
16
  tickTextWidth: number;
16
17
  };
17
- export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
18
- export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): {
18
+ export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number;
19
+ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number): {
19
20
  left: number;
20
21
  right: number;
21
22
  };
@@ -21,6 +21,7 @@ export interface BuildTrackLayoutsOptions {
21
21
  yDomains?: Record<string, [number, number]>;
22
22
  timeUnit: 's' | 'ms';
23
23
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
24
+ yAxisSplitNumber?: number;
24
25
  rendering: ResolvedWaveformRenderingOptions;
25
26
  hideSecondaryLabels: boolean;
26
27
  yAxisLabelX: number;
@@ -150,6 +150,8 @@ export interface WaveformAxesOptions {
150
150
  };
151
151
  y?: {
152
152
  lineVisible?: boolean;
153
+ /** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
154
+ splitNumber?: number;
153
155
  };
154
156
  }
155
157
  /** Styling and visibility options for the horizontal zero-value reference line. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -35,7 +35,7 @@ function track(seriesList: DisplaySeries[]): DisplayTrack {
35
35
  }
36
36
 
37
37
  describe('fixed Y-domain layout', () => {
38
- it('uses an exact global fixed domain without applying nice bounds', () => {
38
+ it('expands a global fixed domain to nice equal intervals', () => {
39
39
  const sourceTrack = track([series('a', 0, 100)])
40
40
  const result = buildTrackLayouts({
41
41
  cells: [
@@ -66,10 +66,80 @@ describe('fixed Y-domain layout', () => {
66
66
  showCompactEmptyTracks: false,
67
67
  })[0]
68
68
 
69
- expect(result?.yScale.domain()).toEqual([3, 97])
70
- expect(result?.yAxes[0]?.tickValues).toContain(3)
71
- expect(result?.yAxes[0]?.tickValues).toContain(97)
72
- expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([3, 97])
69
+ expect(result?.yScale.domain()).toEqual([0, 100])
70
+ expect(result?.yAxes[0]?.tickValues).toContain(0)
71
+ expect(result?.yAxes[0]?.tickValues).toContain(100)
72
+ expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([0, 100])
73
+ })
74
+
75
+ it('uses the configured split number for fixed domains', () => {
76
+ const result = buildTrackLayouts({
77
+ cells: [
78
+ {
79
+ slotIndex: 0,
80
+ row: 0,
81
+ column: 0,
82
+ left: 0,
83
+ top: 0,
84
+ width: 120,
85
+ height: 100,
86
+ plotHeight: 100,
87
+ cellHeight: 130,
88
+ xAxisBand: 30,
89
+ series: track([series('a', 0, 100)]),
90
+ },
91
+ ],
92
+ grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
93
+ displayMode: 'independent',
94
+ overlayMode: 'single-axis',
95
+ independentTransforms: [zoomIdentity],
96
+ sharedZoomDomain: [0, 1],
97
+ fixedYDomain: [3, 97],
98
+ yAxisSplitNumber: 5,
99
+ timeUnit: 'ms',
100
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
101
+ hideSecondaryLabels: false,
102
+ yAxisLabelX: -50,
103
+ showCompactEmptyTracks: false,
104
+ })[0]
105
+
106
+ expect(result?.yAxes[0]?.majorTicks).toEqual([0, 25, 50, 75, 100])
107
+ })
108
+
109
+ it('defaults to five ticks and supports a two-tick axis', () => {
110
+ const build = (splitNumber?: number) =>
111
+ buildTrackLayouts({
112
+ cells: [
113
+ {
114
+ slotIndex: 0,
115
+ row: 0,
116
+ column: 0,
117
+ left: 0,
118
+ top: 0,
119
+ width: 120,
120
+ height: 100,
121
+ plotHeight: 100,
122
+ cellHeight: 130,
123
+ xAxisBand: 30,
124
+ series: track([series('a', 3, 97)]),
125
+ },
126
+ ],
127
+ grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
128
+ displayMode: 'independent',
129
+ overlayMode: 'single-axis',
130
+ independentTransforms: [zoomIdentity],
131
+ sharedZoomDomain: [0, 1],
132
+ fixedYDomain: [3, 97],
133
+ yAxisSplitNumber: splitNumber,
134
+ timeUnit: 'ms',
135
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
136
+ hideSecondaryLabels: false,
137
+ yAxisLabelX: -50,
138
+ showCompactEmptyTracks: false,
139
+ })[0]?.yAxes[0]?.majorTicks
140
+
141
+ expect(build()).toHaveLength(5)
142
+ expect(build(2)).toEqual([0, 100])
73
143
  })
74
144
 
75
145
  it('resolves track, series, and global fixed-domain precedence', () => {
@@ -9,6 +9,7 @@ import {
9
9
  findClosestTrackAtPointer,
10
10
  MAX_MULTI_Y_AXIS_COUNT,
11
11
  measureYAxisGroupClearance,
12
+ resolveYAxisTickCount,
12
13
  } from './layout'
13
14
 
14
15
  function series(id: string, minimum: number, maximum: number): DisplaySeries {
@@ -119,8 +120,15 @@ describe('multi-value Y-axis grouping', () => {
119
120
  showCompactEmptyTracks: false,
120
121
  })[0]
121
122
 
122
- expect(result?.yScale.domain()).toEqual([25, 75])
123
- expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([25, 75])
123
+ expect(result?.yScale.domain()).toEqual([20, 80])
124
+ expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([20, 80])
125
+ })
126
+
127
+ it('normalizes configured Y-axis split counts', () => {
128
+ expect(resolveYAxisTickCount(100, 0)).toBe(2)
129
+ expect(resolveYAxisTickCount(100, 4.8)).toBe(4)
130
+ expect(resolveYAxisTickCount(100, Number.NaN)).toBe(5)
131
+ expect(resolveYAxisTickCount(220)).toBe(5)
124
132
  })
125
133
 
126
134
  it('keeps every overlaid series on one axis in single-axis mode', () => {
@@ -20,6 +20,13 @@ const Y_AXIS_OUTER_PADDING = 4
20
20
  const Y_AXIS_LABEL_GAP = 6
21
21
  const Y_AXIS_LABEL_BAND_WIDTH = 24
22
22
 
23
+ export function resolveYAxisTickCount(_plotHeight: number, splitNumber?: number): number {
24
+ if (typeof splitNumber === 'number' && Number.isFinite(splitNumber)) {
25
+ return Math.max(2, Math.floor(splitNumber))
26
+ }
27
+ return 5
28
+ }
29
+
23
30
  export interface YAxisSeriesGroup {
24
31
  index: number
25
32
  side: 'left' | 'right'
@@ -104,11 +111,16 @@ export function axisTextMetrics(
104
111
  nice = true,
105
112
  tickValues?: number[],
106
113
  unit?: string,
114
+ tickCount = 10,
107
115
  ): { tickTextWidth: number } {
116
+ const effectiveTickCount =
117
+ typeof tickCount === 'number' && Number.isFinite(tickCount)
118
+ ? Math.max(2, Math.floor(tickCount))
119
+ : 10
108
120
  const scale = scaleLinear(domain, [1, 0])
109
- if (nice) scale.nice()
121
+ if (nice) scale.nice(effectiveTickCount)
110
122
  const [axisMin, axisMax] = scale.domain()
111
- const values = tickValues ?? Array.from(new Set([axisMin, ...scale.ticks(10), axisMax]))
123
+ const values = tickValues ?? scale.ticks(effectiveTickCount)
112
124
  const topTickValue = Math.max(...values)
113
125
  const maximumTickCharacters = Math.max(
114
126
  1,
@@ -121,9 +133,9 @@ export function axisTextMetrics(
121
133
  }
122
134
  }
123
135
 
124
- export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
136
+ export function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number {
125
137
  return (
126
- axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
138
+ axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
127
139
  .tickTextWidth +
128
140
  Y_AXIS_TICK_PADDING +
129
141
  Y_AXIS_LABEL_GAP +
@@ -132,9 +144,9 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
132
144
  )
133
145
  }
134
146
 
135
- function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
147
+ function measureYAxisGroupTickClearance(group: YAxisSeriesGroup, tickCount?: number): number {
136
148
  return (
137
- axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
149
+ axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
138
150
  .tickTextWidth +
139
151
  Y_AXIS_TICK_PADDING +
140
152
  Y_AXIS_OUTER_PADDING
@@ -146,13 +158,14 @@ export function measureTrackYAxisClearance(
146
158
  overlayMode: WaveformOverlayMode,
147
159
  yDomain?: WaveformYDomain,
148
160
  yDomains?: Record<string, WaveformYDomain>,
161
+ tickCount?: number,
149
162
  ): { left: number; right: number } {
150
163
  return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).reduce(
151
164
  (clearance, group) => {
152
165
  clearance[group.side] +=
153
166
  overlayMode === 'multi-axis' || track.visibleSeries.length === 1
154
- ? measureYAxisGroupClearance(group)
155
- : measureYAxisGroupTickClearance(group)
167
+ ? measureYAxisGroupClearance(group, tickCount)
168
+ : measureYAxisGroupTickClearance(group, tickCount)
156
169
  return clearance
157
170
  },
158
171
  { left: 0, right: 0 },
@@ -25,7 +25,7 @@ import {
25
25
  type GridCellGeometry,
26
26
  type NormalizedWaveformGridOptions,
27
27
  } from './grid'
28
- import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
28
+ import { axisTextMetrics, resolveYAxisSeriesGroups, resolveYAxisTickCount } from './layout'
29
29
  import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
30
30
  import { applyXDomainStrategy } from './xDomain'
31
31
  import {
@@ -54,6 +54,7 @@ export interface BuildTrackLayoutsOptions {
54
54
  yDomains?: Record<string, [number, number]>
55
55
  timeUnit: 's' | 'ms'
56
56
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter
57
+ yAxisSplitNumber?: number
57
58
  rendering: ResolvedWaveformRenderingOptions
58
59
  hideSecondaryLabels: boolean
59
60
  yAxisLabelX: number
@@ -124,22 +125,26 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
124
125
  )
125
126
  const sideOffsets = { left: 0, right: 0 }
126
127
  const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
128
+ const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
129
+ const niceCount = Math.max(1, tickCount - 1)
127
130
  const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
128
- if (!group.fixed) scale.nice()
129
- const majorTicks = scale.ticks(Math.max(2, Math.floor(cell.plotHeight / 55)))
131
+ scale.nice(niceCount)
130
132
  const [axisStart, axisEnd] = scale.domain()
133
+ const majorTicks = Array.from(
134
+ { length: tickCount },
135
+ (_, index) => axisStart + ((axisEnd - axisStart) * index) / (tickCount - 1),
136
+ )
131
137
  const showAxisEnd = options.displayMode !== 'compact' || cell.row === 0
132
138
  const visibleMajorTicks = showAxisEnd
133
139
  ? majorTicks
134
140
  : majorTicks.filter((tick) => tick !== axisEnd)
135
- const tickValues = Array.from(
136
- new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
137
- )
141
+ const tickValues = visibleMajorTicks
138
142
  const { tickTextWidth } = axisTextMetrics(
139
- group.domain,
140
- !group.fixed,
143
+ scale.domain() as [number, number],
144
+ false,
141
145
  tickValues,
142
146
  group.seriesList[0]?.unit,
147
+ tickCount,
143
148
  )
144
149
  const clearance =
145
150
  tickTextWidth +
@@ -114,8 +114,13 @@ export function useWaveformLayout(context: LayoutContext) {
114
114
  )
115
115
  .map(
116
116
  (group) =>
117
- axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
118
- .tickTextWidth,
117
+ axisTextMetrics(
118
+ group.domain,
119
+ true,
120
+ undefined,
121
+ group.seriesList[0]?.unit,
122
+ props.axes?.y?.splitNumber,
123
+ ).tickTextWidth,
119
124
  )
120
125
  const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
121
126
  const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
@@ -161,6 +166,7 @@ export function useWaveformLayout(context: LayoutContext) {
161
166
  props.overlayMode,
162
167
  props.yDomain,
163
168
  props.yDomains,
169
+ props.axes?.y?.splitNumber,
164
170
  )
165
171
  return {
166
172
  left: Math.max(maximum.left, clearance.left),
@@ -302,6 +308,7 @@ export function useWaveformLayout(context: LayoutContext) {
302
308
  : sharedYDomains.value,
303
309
  timeUnit: props.timeUnit,
304
310
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
311
+ yAxisSplitNumber: props.axes?.y?.splitNumber,
305
312
  rendering: renderingOptions.value,
306
313
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
307
314
  yAxisLabelX: yAxisMetrics.value.labelCenterX,
@@ -65,9 +65,9 @@ describe('WaveformChart', () => {
65
65
  )
66
66
 
67
67
  expect(endTicks[0]).toHaveLength(1)
68
- expect(Number(endTicks[0][0].text())).toBe(0.9)
68
+ expect(Number(endTicks[0][0].text())).toBe(1)
69
69
  expect(endTicks[1]).toHaveLength(1)
70
- expect(Number(endTicks[1][0].text())).toBe(1.9)
70
+ expect(Number(endTicks[1][0].text())).toBe(2)
71
71
  expect(endTicks[2]).toHaveLength(0)
72
72
  expect(endTicks[3]).toHaveLength(0)
73
73
  })
@@ -261,7 +261,7 @@ describe('WaveformChart', () => {
261
261
  })
262
262
 
263
263
  expect(startTicks).toHaveLength(1)
264
- expect(Number(startTicks[0].text())).toBe(0.1)
264
+ expect(Number(startTicks[0].text())).toBe(0)
265
265
  },
266
266
  )
267
267
 
@@ -297,7 +297,7 @@ describe('WaveformChart', () => {
297
297
  { displayMode },
298
298
  )
299
299
 
300
- const expectedEndValues = [0.9, 4.9]
300
+ const expectedEndValues = [1, 5]
301
301
  wrapper.findAll('.waveform-chart__track').forEach((track, index) => {
302
302
  const endTicks = track.findAll('.waveform-chart__axis--y .tick').filter((tick) => {
303
303
  const match = tick.attributes('transform')?.match(/translate\(0,\s*([\d.]+)\)/)
@@ -6,6 +6,26 @@ 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
+
9
29
  it('expands the Y-axis label gutter for signed values and long exponents', async () => {
10
30
  const wrapper = await mountSizedChart(
11
31
  {
@@ -296,7 +316,7 @@ describe('WaveformChart', () => {
296
316
  expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
297
317
  })
298
318
 
299
- it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
319
+ it('reacts to nice fixed Y-domain props and returns to automatic bounds', async () => {
300
320
  const wrapper = await mountSizedChart({
301
321
  kind: 'points',
302
322
  points: [
@@ -312,8 +332,8 @@ describe('WaveformChart', () => {
312
332
 
313
333
  await wrapper.setProps({ yDomain: [3, 97] })
314
334
  await flushPromises()
315
- expect(yTickLabels()).toContain('3')
316
- expect(yTickLabels()).toContain('97')
335
+ expect(yTickLabels()).toContain('0')
336
+ expect(yTickLabels()).toContain('100')
317
337
 
318
338
  await wrapper.setProps({ yDomain: undefined })
319
339
  await flushPromises()
@@ -174,6 +174,8 @@ export interface WaveformAxesOptions {
174
174
  }
175
175
  y?: {
176
176
  lineVisible?: boolean
177
+ /** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
178
+ splitNumber?: number
177
179
  }
178
180
  }
179
181