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.
- package/README.md +10 -7
- package/dist/index.cjs +2 -2
- package/dist/index.js +769 -765
- package/dist/types/components/core/layout.d.ts +2 -1
- package/dist/types/components/core/yAxisTitleLayout.d.ts +1 -1
- package/dist/types/components/interaction/useExtremaTickViewport.d.ts +1 -1
- package/dist/types/demo/types.d.ts +2 -1
- package/dist/types/types/chart.d.ts +4 -2
- package/package.json +1 -1
- package/src/App.vue +6 -3
- package/src/components/core/layout.ts +36 -22
- package/src/components/core/trackLayoutBuilder.ts +8 -5
- package/src/components/core/useWaveformLayout.ts +3 -1
- package/src/components/core/yAxisLayoutMetrics.ts +24 -16
- package/src/components/core/yAxisPadding.ts +10 -5
- package/src/components/core/yAxisTitleLayout.ts +9 -48
- package/src/components/interaction/useExtremaTickViewport.ts +1 -1
- package/src/demo/DemoYAxisPaddingControls.vue +11 -3
- package/src/demo/types.ts +2 -1
- package/src/index.ts +1 -1
- package/src/types/chart.ts +4 -2
|
@@ -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
|
-
/**
|
|
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[],
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
254
|
-
|
|
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
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
233
|
-
Y_AXIS_CHARACTER_WIDTH
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
)
|
|
338
|
-
widths.set(key, Math.max(widths.get(key) ?? Y_AXIS_CHARACTER_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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
() =>
|
|
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
|
-
(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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(
|
|
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
|
-
|
|
87
|
+
titleTextWidth +
|
|
82
88
|
Y_AXIS_LABEL_GAP +
|
|
83
89
|
labelBandWidth / 2
|
|
84
90
|
)
|
|
85
91
|
const fullClearance =
|
|
86
|
-
tickTextWidth
|
|
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
|
|
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
|
|
30
|
+
config,
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
function withYAxisExtremaTicks(
|
|
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:
|
|
44
|
-
|
|
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
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
85
|
-
|
|
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: () =>
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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.
|
|
43
|
+
// Includes axes.y.maximumTickVisible and minimumTickVisible for independent extrema ticks.
|
|
44
44
|
WaveformAxesOptions,
|
|
45
45
|
WaveformYAxisLabelTextStyle,
|
|
46
46
|
WaveformZeroLineOptions,
|
package/src/types/chart.ts
CHANGED
|
@@ -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
|
|
293
|
-
|
|
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. */
|