waveform-analysis 0.1.73 → 0.1.74

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.
@@ -10,7 +10,7 @@ export interface ResolvedYAxisTicks {
10
10
  }
11
11
  export declare function resolveYAxisTicks(domain: [number, number], tickCount?: number, nice?: boolean): ResolvedYAxisTicks;
12
12
  export declare function formatYAxisTickLabel(value: number, domain: [number, number], tickValues: readonly number[], unit?: string, side?: 'left' | 'right'): string;
13
- /** Scientific multipliers retain their rendered position but do not reserve horizontal space. */
13
+ /** Name spacing excludes scientific multipliers; outer clearance measures the full label. */
14
14
  export declare function formatYAxisTickLayoutLabel(value: number, domain: [number, number], tickValues: readonly number[], unit?: string): string;
15
15
  export interface YAxisSeriesGroup {
16
16
  index: number;
@@ -33,6 +33,7 @@ export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMod
33
33
  export declare function resolveRenderedYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, viewportYDomains?: Record<string, [number, number]>, padding?: YAxisPaddingOptions | number): YAxisSeriesGroup[];
34
34
  export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number, includeWithoutLastTick?: boolean, measureTextWidth?: (text: string) => number, maximumTick?: number, minimumTick?: number): {
35
35
  tickTextWidth: number;
36
+ titleTextWidth: number;
36
37
  };
37
38
  export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number, nice?: boolean): number;
38
39
  export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
@@ -7,5 +7,5 @@ interface TitleLayoutOptions {
7
7
  measureTextWidth?: (text: string) => number;
8
8
  }
9
9
  type TitleTrack = Pick<TrackLayout, 'height' | 'column' | 'yAxes'>;
10
- export declare function alignLeftYAxisTitles(layouts: TitleTrack[], yLabel?: string, options?: TitleLayoutOptions): void;
10
+ export declare function alignLeftYAxisTitles(layouts: TitleTrack[], _yLabel?: string, options?: TitleLayoutOptions): void;
11
11
  export {};
@@ -2,4 +2,4 @@ import { type ComputedRef, type ShallowRef } from 'vue';
2
2
  import type { ZoomTransform } from 'd3';
3
3
  import type { TrackLayout } from '../core/types';
4
4
  /** Keep X coordinates stable when toggling extrema changes the reserved Y-label width. */
5
- export declare function useExtremaTickViewport(visible: () => boolean, innerWidth: ComputedRef<number>, tracks: ComputedRef<TrackLayout[]>, sharedTransform: ShallowRef<ZoomTransform>, independentTransforms: ShallowRef<ZoomTransform[]>): void;
5
+ export declare function useExtremaTickViewport(visible: () => number, innerWidth: ComputedRef<number>, tracks: ComputedRef<TrackLayout[]>, sharedTransform: ShallowRef<ZoomTransform>, independentTransforms: ShallowRef<ZoomTransform[]>): void;
@@ -35,7 +35,8 @@ export interface DemoControlPanelModel {
35
35
  yAxisLineVisible: boolean;
36
36
  upperPaddingEnabled: boolean;
37
37
  lowerPaddingEnabled: boolean;
38
- extremaTicksVisible: boolean;
38
+ maximumTickVisible: boolean;
39
+ minimumTickVisible: boolean;
39
40
  upperPaddingPercent: number;
40
41
  lowerPaddingPercent: number;
41
42
  xAxisLabelFormatterEnabled: boolean;
@@ -250,8 +250,10 @@ export interface WaveformAxesOptions {
250
250
  splitNumber?: number;
251
251
  /** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
252
252
  nice?: boolean;
253
- /** Shows extra ticks for the full data extrema, independently of padding. Defaults to true. */
254
- extremaTicksVisible?: boolean;
253
+ /** Shows the full data maximum's extra tick and label. Defaults to true. */
254
+ maximumTickVisible?: boolean;
255
+ /** Shows the full data minimum's extra tick and label. Defaults to true. */
256
+ minimumTickVisible?: boolean;
255
257
  /** Fraction of the automatic Y span added above its maximum. Defaults to 0. */
256
258
  upperPaddingRatio?: number;
257
259
  /** Fraction of the automatic Y span added below its minimum. Defaults to 0. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.73",
3
+ "version": "0.1.74",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
package/src/App.vue CHANGED
@@ -56,7 +56,8 @@ const xAxisLineVisible = ref(false)
56
56
  const yAxisLineVisible = ref(false)
57
57
  const upperPaddingEnabled = ref(true)
58
58
  const lowerPaddingEnabled = ref(true)
59
- const extremaTicksVisible = ref(true)
59
+ const maximumTickVisible = ref(true)
60
+ const minimumTickVisible = ref(true)
60
61
  const upperPaddingPercent = ref(10)
61
62
  const lowerPaddingPercent = ref(10)
62
63
  const { controlModel: xAxisLabelControlModel, xAxisLabelFormatter } = useDemoXAxisLabelControls()
@@ -145,7 +146,8 @@ const axes = computed<WaveformAxesOptions>(() => ({
145
146
  lineVisible: yAxisLineVisible.value,
146
147
  upperPaddingEnabled: upperPaddingEnabled.value,
147
148
  lowerPaddingEnabled: lowerPaddingEnabled.value,
148
- extremaTicksVisible: extremaTicksVisible.value,
149
+ maximumTickVisible: maximumTickVisible.value,
150
+ minimumTickVisible: minimumTickVisible.value,
149
151
  upperPaddingRatio: upperPaddingPercent.value / 100,
150
152
  lowerPaddingRatio: lowerPaddingPercent.value / 100,
151
153
  },
@@ -367,7 +369,8 @@ const controlPanelModel = reactive({
367
369
  yAxisLineVisible,
368
370
  upperPaddingEnabled,
369
371
  lowerPaddingEnabled,
370
- extremaTicksVisible,
372
+ maximumTickVisible,
373
+ minimumTickVisible,
371
374
  upperPaddingPercent,
372
375
  lowerPaddingPercent,
373
376
  ...xAxisLabelControlModel,
@@ -83,7 +83,7 @@ export function formatYAxisTickLabel(
83
83
  return label
84
84
  }
85
85
 
86
- /** Scientific multipliers retain their rendered position but do not reserve horizontal space. */
86
+ /** Name spacing excludes scientific multipliers; outer clearance measures the full label. */
87
87
  export function formatYAxisTickLayoutLabel(
88
88
  value: number,
89
89
  domain: [number, number],
@@ -215,7 +215,7 @@ export function axisTextMetrics(
215
215
  measureTextWidth?: (text: string) => number,
216
216
  maximumTick?: number,
217
217
  minimumTick?: number,
218
- ): { tickTextWidth: number } {
218
+ ): { tickTextWidth: number; titleTextWidth: number } {
219
219
  const resolvedTicks = tickValues
220
220
  ? { domain, values: tickValues }
221
221
  : resolveYAxisTicks(domain, tickCount, nice)
@@ -229,17 +229,26 @@ export function axisTextMetrics(
229
229
  if (includeWithoutLastTick && resolvedTicks.values.length > 1) {
230
230
  tickValueSets.push(resolvedTicks.values.slice(0, -1))
231
231
  }
232
- const maximumTickWidth = Math.max(
233
- Y_AXIS_CHARACTER_WIDTH,
234
- ...tickValueSets.flatMap((values) =>
235
- values.map((value) => {
236
- const text = formatYAxisTickLayoutLabel(value, resolvedTicks.domain, values, unit)
237
- return measureTextWidth ? measureTextWidth(text) : text.length * Y_AXIS_CHARACTER_WIDTH
238
- }),
239
- ),
232
+ const width = (text: string) =>
233
+ measureTextWidth ? measureTextWidth(text) : text.length * Y_AXIS_CHARACTER_WIDTH
234
+ const labels = tickValueSets.flatMap((values) =>
235
+ values.map((value) => ({
236
+ full: width(formatYAxisTickLabel(value, resolvedTicks.domain, values, unit)),
237
+ title: width(formatYAxisTickLayoutLabel(value, resolvedTicks.domain, values, unit)),
238
+ })),
240
239
  )
240
+ // Collision handling can move metadata to any surviving tick in compact layouts.
241
+ if (includeWithoutLastTick) {
242
+ tickValueSets[0]!.forEach((value) => {
243
+ labels.push({
244
+ full: width(formatYAxisTickLabel(value, resolvedTicks.domain, [value], unit)),
245
+ title: width(formatYAxisTickLayoutLabel(value, resolvedTicks.domain, [value], unit)),
246
+ })
247
+ })
248
+ }
241
249
  return {
242
- tickTextWidth: maximumTickWidth,
250
+ tickTextWidth: Math.max(Y_AXIS_CHARACTER_WIDTH, ...labels.map((label) => label.full)),
251
+ titleTextWidth: Math.max(Y_AXIS_CHARACTER_WIDTH, ...labels.map((label) => label.title)),
243
252
  }
244
253
  }
245
254
 
@@ -248,12 +257,16 @@ export function measureYAxisGroupClearance(
248
257
  tickCount?: number,
249
258
  nice = true,
250
259
  ): number {
260
+ const { tickTextWidth, titleTextWidth } = axisTextMetrics(
261
+ group.domain,
262
+ nice,
263
+ undefined,
264
+ group.seriesList[0]?.unit,
265
+ tickCount,
266
+ )
251
267
  return (
252
- axisTextMetrics(group.domain, nice, undefined, group.seriesList[0]?.unit, tickCount)
253
- .tickTextWidth +
268
+ Math.max(tickTextWidth, titleTextWidth + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH) +
254
269
  Y_AXIS_TICK_PADDING +
255
- Y_AXIS_LABEL_GAP +
256
- Y_AXIS_LABEL_BAND_WIDTH +
257
270
  Y_AXIS_OUTER_PADDING
258
271
  )
259
272
  }
@@ -312,6 +325,7 @@ export function buildYAxisSlots(
312
325
  typeof padding === 'object' ? padding.labelTextStyle : undefined,
313
326
  )
314
327
  const widths = new Map<string, number>()
328
+ const titleWidths = new Map<string, number>()
315
329
  tracks.forEach((track) => {
316
330
  const sideIndexes = { left: 0, right: 0 }
317
331
  resolveRenderedYAxisSeriesGroups(
@@ -324,7 +338,7 @@ export function buildYAxisSlots(
324
338
  ).forEach((group) => {
325
339
  const sideIndex = sideIndexes[group.side]++
326
340
  const key = `${group.side}:${sideIndex}`
327
- const width = axisTextMetrics(
341
+ const { tickTextWidth, titleTextWidth } = axisTextMetrics(
328
342
  group.domain,
329
343
  nice,
330
344
  undefined,
@@ -334,8 +348,9 @@ export function buildYAxisSlots(
334
348
  measureTextWidth,
335
349
  group.maximumTick,
336
350
  group.minimumTick,
337
- ).tickTextWidth
338
- widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, width))
351
+ )
352
+ widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, tickTextWidth))
353
+ titleWidths.set(key, Math.max(titleWidths.get(key) ?? Y_AXIS_CHARACTER_WIDTH, titleTextWidth))
339
354
  })
340
355
  })
341
356
 
@@ -348,15 +363,14 @@ export function buildYAxisSlots(
348
363
  .sort((first, second) => first - second)
349
364
  sideIndexes.forEach((sideIndex) => {
350
365
  const tickTextWidth = widths.get(`${side}:${sideIndex}`) ?? Y_AXIS_CHARACTER_WIDTH
366
+ const titleTextWidth = titleWidths.get(`${side}:${sideIndex}`) ?? Y_AXIS_CHARACTER_WIDTH
351
367
  const axisOffset = side === 'left' && clearance.left > 0 ? -clearance.left : clearance[side]
352
368
  const labelDistance =
353
- tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + labelBandWidth / 2
369
+ titleTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + labelBandWidth / 2
354
370
  const labelOffset = axisOffset + labelDistance * (side === 'left' ? -1 : 1)
355
371
  const fullClearance =
356
- tickTextWidth +
372
+ Math.max(tickTextWidth, titleTextWidth + Y_AXIS_LABEL_GAP + labelBandWidth) +
357
373
  Y_AXIS_TICK_PADDING +
358
- Y_AXIS_LABEL_GAP +
359
- labelBandWidth +
360
374
  Y_AXIS_OUTER_PADDING
361
375
  slots.push({ side, sideIndex, axisOffset, labelOffset, clearance: fullClearance })
362
376
  clearance[side] += fullClearance
@@ -170,7 +170,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
170
170
  ? group.minimumTick
171
171
  : undefined
172
172
  const tickValues = withMaximumTick(majorTicks, maximumTick, minimumTick)
173
- const { tickTextWidth } = axisTextMetrics(
173
+ const { tickTextWidth, titleTextWidth } = axisTextMetrics(
174
174
  scale.domain() as [number, number],
175
175
  false,
176
176
  tickValues,
@@ -178,10 +178,13 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
178
178
  tickCount,
179
179
  )
180
180
  const clearance =
181
- tickTextWidth +
181
+ Math.max(
182
+ tickTextWidth,
183
+ titleTextWidth +
184
+ Y_AXIS_LABEL_GAP +
185
+ resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle),
186
+ ) +
182
187
  Y_AXIS_TICK_PADDING +
183
- Y_AXIS_LABEL_GAP +
184
- resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) +
185
188
  Y_AXIS_OUTER_PADDING
186
189
  const slot = options.yAxisSlots?.find(
187
190
  (candidate) => candidate.side === group.side && candidate.sideIndex === sideIndex,
@@ -192,7 +195,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
192
195
  ? -sideOffsets.left
193
196
  : cell.width + sideOffsets.right
194
197
  const labelDistance =
195
- tickTextWidth +
198
+ titleTextWidth +
196
199
  Y_AXIS_TICK_PADDING +
197
200
  Y_AXIS_LABEL_GAP +
198
201
  resolveYAxisLabelBandWidth(options.yAxisPadding?.labelTextStyle) / 2
@@ -281,7 +281,9 @@ export function useWaveformLayout(context: LayoutContext) {
281
281
  }),
282
282
  )
283
283
  useExtremaTickViewport(
284
- () => props.axes?.y?.extremaTicksVisible !== false,
284
+ () =>
285
+ Number(props.axes?.y?.maximumTickVisible !== false) * 2 +
286
+ Number(props.axes?.y?.minimumTickVisible !== false),
285
287
  innerWidth,
286
288
  trackLayouts,
287
289
  sharedTransform,
@@ -60,29 +60,37 @@ export function resolveYAxisLayoutMetrics(
60
60
  padding,
61
61
  ),
62
62
  )
63
- .map(
64
- (group) =>
65
- axisTextMetrics(
66
- group.domain,
67
- nice,
68
- undefined,
69
- showUnits ? group.seriesList[0]?.unit : undefined,
70
- tickCount,
71
- compact,
72
- measureTextWidth,
73
- group.maximumTick,
74
- group.minimumTick,
75
- ).tickTextWidth,
63
+ .map((group) =>
64
+ axisTextMetrics(
65
+ group.domain,
66
+ nice,
67
+ undefined,
68
+ showUnits ? group.seriesList[0]?.unit : undefined,
69
+ tickCount,
70
+ compact,
71
+ measureTextWidth,
72
+ group.maximumTick,
73
+ group.minimumTick,
74
+ ),
76
75
  )
77
- const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
76
+ const tickTextWidth = Math.max(
77
+ Y_AXIS_CHARACTER_WIDTH,
78
+ ...axisText.map((text) => text.tickTextWidth),
79
+ )
80
+ const titleTextWidth = Math.max(
81
+ Y_AXIS_CHARACTER_WIDTH,
82
+ ...axisText.map((text) => text.titleTextWidth),
83
+ )
78
84
  const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
79
85
  const labelCenterX = -(
80
86
  Y_AXIS_TICK_PADDING +
81
- tickTextWidth +
87
+ titleTextWidth +
82
88
  Y_AXIS_LABEL_GAP +
83
89
  labelBandWidth / 2
84
90
  )
85
91
  const fullClearance =
86
- tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + labelBandWidth + Y_AXIS_OUTER_PADDING
92
+ Math.max(tickTextWidth, titleTextWidth + Y_AXIS_LABEL_GAP + labelBandWidth) +
93
+ Y_AXIS_TICK_PADDING +
94
+ Y_AXIS_OUTER_PADDING
87
95
  return { tickClearance, fullClearance, labelCenterX }
88
96
  }
@@ -15,7 +15,7 @@ export function applyYAxisPadding(
15
15
  options: YAxisPaddingOptions | number,
16
16
  ): YAxisSeriesGroup {
17
17
  const config = typeof options === 'number' ? { upperPaddingRatio: options } : options
18
- if (group.fixed) return withYAxisExtremaTicks(group, config?.extremaTicksVisible)
18
+ if (group.fixed) return withYAxisExtremaTicks(group, config)
19
19
  const upper = paddingRatio(config?.upperPaddingEnabled, config?.upperPaddingRatio)
20
20
  const lower = paddingRatio(config?.lowerPaddingEnabled, config?.lowerPaddingRatio)
21
21
  const [a, b] = group.domain
@@ -27,11 +27,14 @@ export function applyYAxisPadding(
27
27
  ...group,
28
28
  domain: viewport ?? [Number.isFinite(min) ? min : a, Number.isFinite(max) ? max : b],
29
29
  },
30
- config?.extremaTicksVisible,
30
+ config,
31
31
  )
32
32
  }
33
33
 
34
- function withYAxisExtremaTicks(group: YAxisSeriesGroup, visible = true): YAxisSeriesGroup {
34
+ function withYAxisExtremaTicks(
35
+ group: YAxisSeriesGroup,
36
+ options: YAxisPaddingOptions,
37
+ ): YAxisSeriesGroup {
35
38
  const maxima = group.seriesList.flatMap((series) =>
36
39
  Number.isFinite(series.rawYMaximum) ? [series.rawYMaximum!] : [],
37
40
  )
@@ -40,8 +43,10 @@ function withYAxisExtremaTicks(group: YAxisSeriesGroup, visible = true): YAxisSe
40
43
  )
41
44
  return {
42
45
  ...group,
43
- maximumTick: visible && maxima.length ? Math.max(...maxima) : undefined,
44
- minimumTick: visible && minima.length ? Math.min(...minima) : undefined,
46
+ maximumTick:
47
+ options?.maximumTickVisible !== false && maxima.length ? Math.max(...maxima) : undefined,
48
+ minimumTick:
49
+ options?.minimumTickVisible !== false && minima.length ? Math.min(...minima) : undefined,
45
50
  }
46
51
  }
47
52
 
@@ -1,13 +1,9 @@
1
1
  import { formatYAxisTickLayoutLabel } from './layout'
2
2
  import type { TrackLayout, WaveformYAxisLayout } from './types'
3
3
  import type { WaveformYAxisLabelTextStyle } from '../../types'
4
- import { resolveYAxisLabelBandWidth, resolveYAxisLabelFontSize } from './yAxisLabelStyle'
4
+ import { resolveYAxisLabelBandWidth } from './yAxisLabelStyle'
5
5
  import { Y_AXIS_CHARACTER_WIDTH, Y_AXIS_TICK_PADDING } from './yAxisConstants'
6
6
 
7
- // Match the 11px ticks in WaveformTrack.css.
8
- const TICK_FONT_SIZE = 11
9
- const TEXT_GAP = 2
10
-
11
7
  interface TitleLayoutOptions {
12
8
  labelTextStyle?: WaveformYAxisLabelTextStyle
13
9
  showUnits?: boolean
@@ -15,52 +11,20 @@ interface TitleLayoutOptions {
15
11
  measureTextWidth?: (text: string) => number
16
12
  }
17
13
 
18
- function estimateTitleWidth(title: string, fontSize: number): number {
19
- return Array.from(title).reduce((width, character) => {
20
- // Latin labels average well below one em; full-width characters occupy one em.
21
- const em = /[\u2e80-\u9fff\uf900-\ufaff\uff01-\uff60]/u.test(character)
22
- ? 1
23
- : /[MW@%]/u.test(character)
24
- ? 0.95
25
- : /[ilI1., :;|'!]/u.test(character)
26
- ? 0.4
27
- : 0.65
28
- return width + em * fontSize
29
- }, 0)
30
- }
31
-
32
14
  type TitleTrack = Pick<TrackLayout, 'height' | 'column' | 'yAxes'>
33
15
 
34
- function titleDistance(
35
- track: TitleTrack,
36
- axis: WaveformYAxisLayout,
37
- yLabel: string | undefined,
38
- options: TitleLayoutOptions,
39
- ): number {
16
+ function titleDistance(axis: WaveformYAxisLayout, options: TitleLayoutOptions): number {
40
17
  const series = axis.seriesList[0]
41
- const title = series?.name.trim() || yLabel || ''
42
- const halfTitleHeight =
43
- estimateTitleWidth(title, resolveYAxisLabelFontSize(options.labelTextStyle)) / 2
44
- const titleTop = track.height / 2 - halfTitleHeight - TEXT_GAP
45
- const titleBottom = track.height / 2 + halfTitleHeight + TEXT_GAP
46
- const lowestTick = Math.min(...axis.tickValues)
47
- const highestTick = Math.max(...axis.tickValues)
48
- const tickWidths = axis.tickValues.flatMap((value) => {
49
- // D3 places ticks at scale(value) + 0.5. The lowest label is bottom-aligned;
50
- // all other labels are vertically centered using dy="0.32em".
51
- const y = axis.scale(value) + 0.5
52
- const isTopAligned = options.compact && value === highestTick
53
- const top = y - (value === lowestTick ? TICK_FONT_SIZE : isTopAligned ? 0 : TICK_FONT_SIZE / 2)
54
- const bottom =
55
- value === lowestTick ? y : y + (isTopAligned ? TICK_FONT_SIZE : TICK_FONT_SIZE / 2)
56
- if (bottom < titleTop || top > titleBottom) return []
18
+ // Reserve the widest tick, including extrema, regardless of the frame height.
19
+ // The layout formatter excludes scientific multipliers but retains the numeric label.
20
+ const tickWidths = axis.tickValues.map((value) => {
57
21
  const text = formatYAxisTickLayoutLabel(
58
22
  value,
59
23
  axis.scale.domain() as [number, number],
60
24
  axis.tickValues,
61
25
  options.showUnits === false ? undefined : series?.unit,
62
26
  )
63
- return [options.measureTextWidth?.(text) ?? text.length * Y_AXIS_CHARACTER_WIDTH]
27
+ return options.measureTextWidth?.(text) ?? text.length * Y_AXIS_CHARACTER_WIDTH
64
28
  })
65
29
  // The title band leaves 4px beside each glyph.
66
30
  return (
@@ -72,7 +36,7 @@ function titleDistance(
72
36
 
73
37
  export function alignLeftYAxisTitles(
74
38
  layouts: TitleTrack[],
75
- yLabel?: string,
39
+ _yLabel?: string,
76
40
  options: TitleLayoutOptions = {},
77
41
  ): void {
78
42
  const distances = new Map<string, number>()
@@ -81,11 +45,8 @@ export function alignLeftYAxisTitles(
81
45
  for (const axis of track.yAxes) {
82
46
  if (axis.side !== 'left') continue
83
47
  const key = `${track.column}:${leftIndex++}`
84
- const distance = titleDistance(track, axis, yLabel, options)
85
- // Compact slots already reserve every tick. Only move names inward, so
86
- // alignment can never push them beyond the space reserved at the chart edge.
87
- const boundedDistance = options.compact ? Math.min(axis.x - axis.labelX, distance) : distance
88
- distances.set(key, Math.max(distances.get(key) ?? 0, boundedDistance))
48
+ const distance = titleDistance(axis, options)
49
+ distances.set(key, Math.max(distances.get(key) ?? 0, distance))
89
50
  }
90
51
  }
91
52
  for (const track of layouts) {
@@ -5,7 +5,7 @@ import type { TrackLayout } from '../core/types'
5
5
 
6
6
  /** Keep X coordinates stable when toggling extrema changes the reserved Y-label width. */
7
7
  export function useExtremaTickViewport(
8
- visible: () => boolean,
8
+ visible: () => number,
9
9
  innerWidth: ComputedRef<number>,
10
10
  tracks: ComputedRef<TrackLayout[]>,
11
11
  sharedTransform: ShallowRef<ZoomTransform>,
@@ -24,9 +24,17 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
24
24
  :aria-label="side === 'upper' ? '上方留白比例' : '下方留白比例'"
25
25
  />
26
26
  </div>
27
- <div class="y-axis-padding-control extrema-control">
28
- <span>显示最值刻度</span>
29
- <Switch v-model:checked="model.extremaTicksVisible" size="small" aria-label="显示最值刻度" />
27
+ <div
28
+ v-for="side in ['maximum', 'minimum'] as const"
29
+ :key="side"
30
+ class="y-axis-padding-control extrema-control"
31
+ >
32
+ <span>{{ side === 'maximum' ? '显示最大值刻度' : '显示最小值刻度' }}</span>
33
+ <Switch
34
+ v-model:checked="model[`${side}TickVisible`]"
35
+ size="small"
36
+ :aria-label="side === 'maximum' ? '显示最大值刻度' : '显示最小值刻度'"
37
+ />
30
38
  </div>
31
39
  <small>仅作用于自动 Y 范围;刻度取整可能增加实际留白。</small>
32
40
  </section>
package/src/demo/types.ts CHANGED
@@ -53,7 +53,8 @@ export interface DemoControlPanelModel {
53
53
  yAxisLineVisible: boolean
54
54
  upperPaddingEnabled: boolean
55
55
  lowerPaddingEnabled: boolean
56
- extremaTicksVisible: boolean
56
+ maximumTickVisible: boolean
57
+ minimumTickVisible: boolean
57
58
  upperPaddingPercent: number
58
59
  lowerPaddingPercent: number
59
60
  xAxisLabelFormatterEnabled: boolean
package/src/index.ts CHANGED
@@ -40,7 +40,7 @@ export type {
40
40
  WaveformXAxisLabelKind,
41
41
  WaveformXAxisLabelFormatterContext,
42
42
  WaveformXAxisLabelFormatter,
43
- // Includes axes.y.extremaTicksVisible for independent extrema tick visibility.
43
+ // Includes axes.y.maximumTickVisible and minimumTickVisible for independent extrema ticks.
44
44
  WaveformAxesOptions,
45
45
  WaveformYAxisLabelTextStyle,
46
46
  WaveformZeroLineOptions,
@@ -289,8 +289,10 @@ export interface WaveformAxesOptions {
289
289
  splitNumber?: number
290
290
  /** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
291
291
  nice?: boolean
292
- /** Shows extra ticks for the full data extrema, independently of padding. Defaults to true. */
293
- extremaTicksVisible?: boolean
292
+ /** Shows the full data maximum's extra tick and label. Defaults to true. */
293
+ maximumTickVisible?: boolean
294
+ /** Shows the full data minimum's extra tick and label. Defaults to true. */
295
+ minimumTickVisible?: boolean
294
296
  /** Fraction of the automatic Y span added above its maximum. Defaults to 0. */
295
297
  upperPaddingRatio?: number
296
298
  /** Fraction of the automatic Y span added below its minimum. Defaults to 0. */