waveform-analysis 0.1.47 → 0.1.48

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/dist/index.js CHANGED
@@ -1135,7 +1135,7 @@ function pn(e) {
1135
1135
  right: 0
1136
1136
  }, p = d.map((n) => {
1137
1137
  let r = yn(t.plotHeight, e.yAxisSplitNumber), i = Math.max(1, r - 1), a = V(n.domain, [t.plotHeight, 0]);
1138
- a.nice(i);
1138
+ e.yAxisNice !== !1 && a.nice(i);
1139
1139
  let [o, s] = a.domain(), c = Array.from({ length: r }, (e, t) => o + (s - o) * t / (r - 1)), l = e.displayMode !== "compact" || t.row === 0 ? c : c.slice(0, -1), { tickTextWidth: u } = wn(a.domain(), !1, l, n.seriesList[0]?.unit, r), d = u + 7 + 0 + 12 + 4, p = n.side === "left" ? -f.left : t.width + f.right, m = u + 7 + 0 + 12 / 2, h = p + (n.side === "left" ? -m : m);
1140
1140
  return f[n.side] += d, {
1141
1141
  index: n.index,
@@ -1148,14 +1148,16 @@ function pn(e) {
1148
1148
  tickValues: l,
1149
1149
  seriesList: n.seriesList
1150
1150
  };
1151
- }), m = p[0]?.scale ?? V(a.yDomain, [t.plotHeight, 0]).nice(), h = l.ticks(Math.max(2, Math.floor(t.width / 100))), g = p[0]?.majorTicks ?? [], _ = p[0]?.tickValues ?? [], v = l.domain(), y = {
1152
- start: ut(v[0], v, e.timeUnit, "start", e.xAxisLabelFormatter),
1153
- end: ut(v[1], v, e.timeUnit, "end", e.xAxisLabelFormatter)
1154
- }, b = y.start.length * 7 + 10, x = y.end.length * 7 + 10, S = h.filter((e) => {
1151
+ }), m = V(a.yDomain, [t.plotHeight, 0]);
1152
+ e.yAxisNice !== !1 && m.nice();
1153
+ let h = p[0]?.scale ?? m, g = l.ticks(Math.max(2, Math.floor(t.width / 100))), _ = p[0]?.majorTicks ?? [], v = p[0]?.tickValues ?? [], y = l.domain(), b = {
1154
+ start: ut(y[0], y, e.timeUnit, "start", e.xAxisLabelFormatter),
1155
+ end: ut(y[1], y, e.timeUnit, "end", e.xAxisLabelFormatter)
1156
+ }, x = b.start.length * 7 + 10, S = b.end.length * 7 + 10, C = g.filter((e) => {
1155
1157
  let n = l(e);
1156
- return n > b && n < t.width - x;
1157
- }), C = a.visibleSeries.map((n) => {
1158
- let r = p.find((e) => e.seriesList.some((e) => e.id === n.id)), a = r?.scale ?? m, o = Qt(n.points, v, t.width, e.rendering, {
1158
+ return n > x && n < t.width - S;
1159
+ }), w = a.visibleSeries.map((n) => {
1160
+ let r = p.find((e) => e.seriesList.some((e) => e.id === n.id)), a = r?.scale ?? h, o = Qt(n.points, y, t.width, e.rendering, {
1159
1161
  lineVisible: !i && n.lineType !== "none",
1160
1162
  pointVisible: n.pointType !== "none",
1161
1163
  errorBarVisible: n.errorBar.visible,
@@ -1186,17 +1188,17 @@ function pn(e) {
1186
1188
  width: t.width,
1187
1189
  height: t.plotHeight,
1188
1190
  xScale: l,
1189
- yScale: m,
1191
+ yScale: h,
1190
1192
  yAxes: p,
1191
- xMajorTicks: h,
1192
- xMinorTicks: Xe(h, 3, v),
1193
- yMajorTicks: g,
1193
+ xMajorTicks: g,
1194
+ xMinorTicks: Xe(g, 3, y),
1195
+ yMajorTicks: _,
1194
1196
  yMinorTicks: p[0]?.minorTicks ?? [],
1195
- yAxisTickValues: _,
1196
- xAxisTickValues: S,
1197
- endpointLabels: y,
1198
- path: C[0]?.path ?? null,
1199
- seriesPaths: C,
1197
+ yAxisTickValues: v,
1198
+ xAxisTickValues: C,
1199
+ endpointLabels: b,
1200
+ path: w[0]?.path ?? null,
1201
+ seriesPaths: w,
1200
1202
  gridLines: e.grid.trackLines[a.id] ?? {
1201
1203
  horizontal: !0,
1202
1204
  vertical: !0
@@ -1273,14 +1275,14 @@ function wn(e, t = !0, n, r, i = 10) {
1273
1275
  unit: r
1274
1276
  }).length)) * mn };
1275
1277
  }
1276
- function Tn(e, t) {
1277
- return wn(e.domain, !0, void 0, e.seriesList[0]?.unit, t).tickTextWidth + hn + _n + vn + gn;
1278
+ function Tn(e, t, n = !0) {
1279
+ return wn(e.domain, n, void 0, e.seriesList[0]?.unit, t).tickTextWidth + hn + _n + vn + gn;
1278
1280
  }
1279
- function En(e, t) {
1280
- return wn(e.domain, !0, void 0, e.seriesList[0]?.unit, t).tickTextWidth + hn + gn;
1281
+ function En(e, t, n = !0) {
1282
+ return wn(e.domain, n, void 0, e.seriesList[0]?.unit, t).tickTextWidth + hn + gn;
1281
1283
  }
1282
- function Dn(e, t, n, r, i) {
1283
- return Cn(e, t, n, r).reduce((n, r) => (n[r.side] += t === "multi-axis" || e.visibleSeries.length === 1 ? Tn(r, i) : En(r, i), n), {
1284
+ function Dn(e, t, n, r, i, a = !0) {
1285
+ return Cn(e, t, n, r).reduce((n, r) => (n[r.side] += t === "multi-axis" || e.visibleSeries.length === 1 ? Tn(r, i, a) : En(r, i, a), n), {
1284
1286
  left: 0,
1285
1287
  right: 0
1286
1288
  });
@@ -2258,14 +2260,17 @@ function rr(e) {
2258
2260
  };
2259
2261
  });
2260
2262
  }), _ = t(() => Ut(n.rendering)), v = t(() => rn(g.value.length, h.value)), y = t(() => an(g.value, i.value, h.value)), b = t(() => {
2261
- let e = g.value.filter((e) => e.visibleSeries.length > 0).flatMap((e) => Cn(e, n.overlayMode, n.yDomain, n.yDomains)).map((e) => wn(e.domain, !0, void 0, e.seriesList[0]?.unit, n.axes?.y?.splitNumber).tickTextWidth), t = Math.max(7, ...e), r = t + 7 + 4, i = -(7 + t + 0 + 12 / 2);
2263
+ let e = y.value.filter((e) => e.series.length > 0);
2264
+ return h.value.fillIncompleteLastRow && e.length > 0 ? e : y.value;
2265
+ }), x = t(() => {
2266
+ let e = g.value.filter((e) => e.visibleSeries.length > 0).flatMap((e) => Cn(e, n.overlayMode, n.yDomain, n.yDomains)).map((e) => wn(e.domain, n.axes?.y?.nice !== !1, void 0, e.seriesList[0]?.unit, n.axes?.y?.splitNumber).tickTextWidth), t = Math.max(7, ...e), r = t + 7 + 4, i = -(7 + t + 0 + 12 / 2);
2262
2267
  return {
2263
2268
  tickClearance: r,
2264
2269
  fullClearance: t + 7 + 0 + 12 + 4,
2265
2270
  labelCenterX: i
2266
2271
  };
2267
- }), x = t(() => g.value.some((e) => e.visibleSeries.length === 1 && !!(e.visibleSeries[0]?.name.trim() || n.yLabel))), S = t(() => g.value.some((e) => e.visibleSeries.length > 0)), C = t(() => Math.max(_t.left, x.value ? b.value.fullClearance : S.value ? b.value.tickClearance : 0)), w = t(() => g.value.reduce((e, t) => {
2268
- let r = Dn(t, n.overlayMode, n.yDomain, n.yDomains, n.axes?.y?.splitNumber);
2272
+ }), S = t(() => g.value.some((e) => e.visibleSeries.length === 1 && !!(e.visibleSeries[0]?.name.trim() || n.yLabel))), C = t(() => g.value.some((e) => e.visibleSeries.length > 0)), w = t(() => Math.max(_t.left, S.value ? x.value.fullClearance : C.value ? x.value.tickClearance : 0)), T = t(() => g.value.reduce((e, t) => {
2273
+ let r = Dn(t, n.overlayMode, n.yDomain, n.yDomains, n.axes?.y?.splitNumber, n.axes?.y?.nice !== !1);
2269
2274
  return {
2270
2275
  left: Math.max(e.left, r.left),
2271
2276
  right: Math.max(e.right, r.right)
@@ -2273,34 +2278,34 @@ function rr(e) {
2273
2278
  }, {
2274
2279
  left: 0,
2275
2280
  right: 0
2276
- })), T = t(() => n.overlayMode === "multi-axis" ? Math.max(C.value, w.value.left) : C.value), E = t(() => n.overlayMode === "multi-axis" ? Math.max(_t.right, w.value.right) : _t.right), D = t(() => Math.max(0, o.value - T.value - E.value)), O = t(() => {
2277
- let e = on(n.displayMode), t = h.value.columnCount, r = t > 1, i = Math.max(e, b.value.fullClearance), a = Math.max(e, b.value.tickClearance), o = (D.value - i * Math.max(0, t - 1)) / t >= 120;
2281
+ })), E = t(() => n.overlayMode === "multi-axis" ? Math.max(w.value, T.value.left) : w.value), D = t(() => n.overlayMode === "multi-axis" ? Math.max(_t.right, T.value.right) : _t.right), O = t(() => Math.max(0, o.value - E.value - D.value)), k = t(() => {
2282
+ let e = on(n.displayMode), t = h.value.columnCount, r = t > 1, i = Math.max(e, x.value.fullClearance), a = Math.max(e, x.value.tickClearance), o = (O.value - i * Math.max(0, t - 1)) / t >= 120;
2278
2283
  return {
2279
- horizontalGap: n.overlayMode === "multi-axis" && r && S.value ? Math.max(e, w.value.left + w.value.right) : r && S.value ? x.value && o ? i : a : e,
2280
- hideSecondaryLabels: n.overlayMode !== "multi-axis" && r && x.value && !o
2284
+ horizontalGap: n.overlayMode === "multi-axis" && r && C.value ? Math.max(e, T.value.left + T.value.right) : r && C.value ? S.value && o ? i : a : e,
2285
+ hideSecondaryLabels: n.overlayMode !== "multi-axis" && r && S.value && !o
2281
2286
  };
2282
- }), k = t(() => m.value.length > 0), A = t(() => D.value > 0 && s.value > 0), j = t(() => n.xLabel ?? `时间(${n.timeUnit})`), M = t(() => n.interactionMode), N = t(() => M.value === "zoom" || M.value === void 0), P = t(() => {
2287
+ }), A = t(() => m.value.length > 0), j = t(() => O.value > 0 && s.value > 0), M = t(() => n.xLabel ?? `时间(${n.timeUnit})`), N = t(() => n.interactionMode), P = t(() => N.value !== "annotation"), F = t(() => {
2283
2288
  let e = [];
2284
2289
  return g.value.forEach((t) => {
2285
2290
  t.visibleSeries.length && e.push(t.xDomain[0], t.xDomain[1]);
2286
2291
  }), $(e);
2287
- }), F = t(() => {
2292
+ }), I = t(() => {
2288
2293
  let e = n.initialXDomain;
2289
- return e && Number.isFinite(e[0]) && Number.isFinite(e[1]) && e[0] !== e[1] ? dn(e[0] < e[1] ? e : [e[1], e[0]], n.xDomainStrategy, !0) : dn(P.value, n.xDomainStrategy);
2290
- }), I = (e) => {
2294
+ return e && Number.isFinite(e[0]) && Number.isFinite(e[1]) && e[0] !== e[1] ? dn(e[0] < e[1] ? e : [e[1], e[0]], n.xDomainStrategy, !0) : dn(F.value, n.xDomainStrategy);
2295
+ }), L = (e) => {
2291
2296
  let t = n.initialXDomains?.[e.series?.trackId ?? e.series?.id ?? e.id] ?? (e.series ? n.initialXDomains?.[e.series.id] : void 0) ?? n.initialXDomain;
2292
2297
  return t && Number.isFinite(t[0]) && Number.isFinite(t[1]) && t[0] !== t[1] ? dn(t[0] < t[1] ? t : [t[1], t[0]], n.xDomainStrategy, !0) : dn($(e.seriesList.flatMap((e) => e.xDomain)), n.xDomainStrategy);
2293
- }, L = t(() => l.value.rescaleX(V(F.value, [0, D.value])).domain()), R = t(() => sn(D.value, s.value, h.value, n.displayMode, y.value.map(Boolean), O.value.horizontalGap).map((e, t) => ({
2298
+ }, R = t(() => l.value.rescaleX(V(I.value, [0, O.value])).domain()), z = t(() => sn(O.value, s.value, h.value, n.displayMode, b.value.map(Boolean), k.value.horizontalGap).map((e, t) => ({
2294
2299
  ...e,
2295
- series: y.value[t]
2296
- }))), z = t(() => pn({
2297
- cells: R.value,
2300
+ series: b.value[t]
2301
+ }))), B = t(() => pn({
2302
+ cells: z.value,
2298
2303
  grid: h.value,
2299
2304
  displayMode: n.displayMode,
2300
2305
  overlayMode: n.overlayMode,
2301
2306
  independentTransforms: u.value,
2302
- sharedZoomDomain: L.value,
2303
- initialXDomain: n.initialXDomain ? F.value : void 0,
2307
+ sharedZoomDomain: R.value,
2308
+ initialXDomain: n.initialXDomain ? I.value : void 0,
2304
2309
  initialXDomains: n.initialXDomains,
2305
2310
  xDomainStrategy: n.xDomainStrategy,
2306
2311
  fixedYDomain: n.yDomain,
@@ -2312,38 +2317,39 @@ function rr(e) {
2312
2317
  timeUnit: n.timeUnit,
2313
2318
  xAxisLabelFormatter: n.axes?.x?.labelFormatter,
2314
2319
  yAxisSplitNumber: n.axes?.y?.splitNumber,
2320
+ yAxisNice: n.axes?.y?.nice,
2315
2321
  rendering: _.value,
2316
- hideSecondaryLabels: c.value || O.value.hideSecondaryLabels,
2317
- yAxisLabelX: b.value.labelCenterX,
2318
- showCompactEmptyTracks: n.displayMode === "compact" && k.value
2319
- })), B = (e) => e.seriesList.map((t) => ({
2322
+ hideSecondaryLabels: c.value || k.value.hideSecondaryLabels,
2323
+ yAxisLabelX: x.value.labelCenterX,
2324
+ showCompactEmptyTracks: n.displayMode === "compact" && A.value
2325
+ })), H = (e) => e.seriesList.map((t) => ({
2320
2326
  ...e,
2321
2327
  series: t,
2322
2328
  yScale: e.seriesPaths.find((e) => e.series.id === t.id)?.yScale ?? e.yScale
2323
- })), H = (e, t) => e.seriesPaths.find((e) => e.series.id === t)?.yScale ?? e.yScale, U = t(() => z.value.flatMap(B));
2329
+ })), U = (e, t) => e.seriesPaths.find((e) => e.series.id === t)?.yScale ?? e.yScale, ee = t(() => B.value.flatMap(H));
2324
2330
  return {
2325
2331
  chartSeries: m,
2326
2332
  chartTracks: g,
2327
2333
  gridOptions: h,
2328
2334
  pageCount: v,
2329
2335
  pagedTracks: y,
2330
- resolvedChartLeftMargin: T,
2331
- innerWidth: D,
2332
- hasVisibleWaveformData: S,
2333
- hasWaveformData: k,
2334
- hasChartArea: A,
2335
- resolvedXLabel: j,
2336
- activeInteractionMode: M,
2337
- isZoomMode: N,
2338
- initialXDomain: F,
2339
- sharedZoomDomain: L,
2340
- resolveInitialTrackDomain: I,
2341
- gridCells: R,
2342
- trackLayouts: z,
2343
- annotationLayoutsForTrack: B,
2344
- resolveSeriesYScale: H,
2345
- annotationTrackLayouts: U,
2346
- renderedAnnotations: t(() => n.annotationsVisible ? ge(n.annotations, U.value, D.value, s.value) : []),
2336
+ resolvedChartLeftMargin: E,
2337
+ innerWidth: O,
2338
+ hasVisibleWaveformData: C,
2339
+ hasWaveformData: A,
2340
+ hasChartArea: j,
2341
+ resolvedXLabel: M,
2342
+ activeInteractionMode: N,
2343
+ isZoomMode: P,
2344
+ initialXDomain: I,
2345
+ sharedZoomDomain: R,
2346
+ resolveInitialTrackDomain: L,
2347
+ gridCells: z,
2348
+ trackLayouts: B,
2349
+ annotationLayoutsForTrack: H,
2350
+ resolveSeriesYScale: U,
2351
+ annotationTrackLayouts: ee,
2352
+ renderedAnnotations: t(() => n.annotationsVisible ? ge(n.annotations, ee.value, O.value, s.value) : []),
2347
2353
  editorSeries: t(() => {
2348
2354
  let e = p.editorDraft.value?.annotation.seriesId, t = m.value.find((t) => t.id === e);
2349
2355
  return t ? {
@@ -15,8 +15,8 @@ export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMod
15
15
  export declare function axisTextMetrics(domain: [number, number], nice?: boolean, tickValues?: number[], unit?: string, tickCount?: number): {
16
16
  tickTextWidth: number;
17
17
  };
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): {
18
+ export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number, nice?: boolean): number;
19
+ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>, tickCount?: number, nice?: boolean): {
20
20
  left: number;
21
21
  right: number;
22
22
  };
@@ -22,6 +22,7 @@ export interface BuildTrackLayoutsOptions {
22
22
  timeUnit: 's' | 'ms';
23
23
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
24
24
  yAxisSplitNumber?: number;
25
+ yAxisNice?: boolean;
25
26
  rendering: ResolvedWaveformRenderingOptions;
26
27
  hideSecondaryLabels: boolean;
27
28
  yAxisLabelX: number;
@@ -152,6 +152,8 @@ export interface WaveformAxesOptions {
152
152
  lineVisible?: boolean;
153
153
  /** Target total number of equal Y-axis ticks, including endpoints. Defaults to 5. */
154
154
  splitNumber?: number;
155
+ /** Expands Y-axis domains to equal, human-friendly intervals. Defaults to true. */
156
+ nice?: boolean;
155
157
  };
156
158
  }
157
159
  /** 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.47",
3
+ "version": "0.1.48",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -133,9 +133,13 @@ export function axisTextMetrics(
133
133
  }
134
134
  }
135
135
 
136
- export function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?: number): number {
136
+ export function measureYAxisGroupClearance(
137
+ group: YAxisSeriesGroup,
138
+ tickCount?: number,
139
+ nice = true,
140
+ ): number {
137
141
  return (
138
- axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
142
+ axisTextMetrics(group.domain, nice, undefined, group.seriesList[0]?.unit, tickCount)
139
143
  .tickTextWidth +
140
144
  Y_AXIS_TICK_PADDING +
141
145
  Y_AXIS_LABEL_GAP +
@@ -144,9 +148,13 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup, tickCount?:
144
148
  )
145
149
  }
146
150
 
147
- function measureYAxisGroupTickClearance(group: YAxisSeriesGroup, tickCount?: number): number {
151
+ function measureYAxisGroupTickClearance(
152
+ group: YAxisSeriesGroup,
153
+ tickCount?: number,
154
+ nice = true,
155
+ ): number {
148
156
  return (
149
- axisTextMetrics(group.domain, true, undefined, group.seriesList[0]?.unit, tickCount)
157
+ axisTextMetrics(group.domain, nice, undefined, group.seriesList[0]?.unit, tickCount)
150
158
  .tickTextWidth +
151
159
  Y_AXIS_TICK_PADDING +
152
160
  Y_AXIS_OUTER_PADDING
@@ -159,13 +167,14 @@ export function measureTrackYAxisClearance(
159
167
  yDomain?: WaveformYDomain,
160
168
  yDomains?: Record<string, WaveformYDomain>,
161
169
  tickCount?: number,
170
+ nice = true,
162
171
  ): { left: number; right: number } {
163
172
  return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).reduce(
164
173
  (clearance, group) => {
165
174
  clearance[group.side] +=
166
175
  overlayMode === 'multi-axis' || track.visibleSeries.length === 1
167
- ? measureYAxisGroupClearance(group, tickCount)
168
- : measureYAxisGroupTickClearance(group, tickCount)
176
+ ? measureYAxisGroupClearance(group, tickCount, nice)
177
+ : measureYAxisGroupTickClearance(group, tickCount, nice)
169
178
  return clearance
170
179
  },
171
180
  { left: 0, right: 0 },
@@ -55,6 +55,7 @@ export interface BuildTrackLayoutsOptions {
55
55
  timeUnit: 's' | 'ms'
56
56
  xAxisLabelFormatter?: WaveformXAxisLabelFormatter
57
57
  yAxisSplitNumber?: number
58
+ yAxisNice?: boolean
58
59
  rendering: ResolvedWaveformRenderingOptions
59
60
  hideSecondaryLabels: boolean
60
61
  yAxisLabelX: number
@@ -119,7 +120,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
119
120
  const tickCount = resolveYAxisTickCount(cell.plotHeight, options.yAxisSplitNumber)
120
121
  const niceCount = Math.max(1, tickCount - 1)
121
122
  const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
122
- scale.nice(niceCount)
123
+ if (options.yAxisNice !== false) scale.nice(niceCount)
123
124
  const [axisStart, axisEnd] = scale.domain()
124
125
  const majorTicks = Array.from(
125
126
  { length: tickCount },
@@ -158,7 +159,9 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
158
159
  seriesList: group.seriesList,
159
160
  }
160
161
  })
161
- const yScale = yAxes[0]?.scale ?? scaleLinear(displayTrack.yDomain, [cell.plotHeight, 0]).nice()
162
+ const fallbackYScale = scaleLinear(displayTrack.yDomain, [cell.plotHeight, 0])
163
+ if (options.yAxisNice !== false) fallbackYScale.nice()
164
+ const yScale = yAxes[0]?.scale ?? fallbackYScale
162
165
  const xMajorTicks = xScale.ticks(Math.max(2, Math.floor(cell.width / 100)))
163
166
  const yMajorTicks = yAxes[0]?.majorTicks ?? []
164
167
  const yAxisTickValues = yAxes[0]?.tickValues ?? []
@@ -112,6 +112,12 @@ export function useWaveformLayout(context: LayoutContext) {
112
112
  const pagedTracks = computed(() =>
113
113
  paginateSeries(chartTracks.value, currentPage.value, gridOptions.value),
114
114
  )
115
+ const layoutTracks = computed(() => {
116
+ const tracksWithSeries = pagedTracks.value.filter((track) => track.series.length > 0)
117
+ return gridOptions.value.fillIncompleteLastRow && tracksWithSeries.length > 0
118
+ ? tracksWithSeries
119
+ : pagedTracks.value
120
+ })
115
121
  const yAxisMetrics = computed(() => {
116
122
  const axisText = chartTracks.value
117
123
  .filter((track) => track.visibleSeries.length > 0)
@@ -122,7 +128,7 @@ export function useWaveformLayout(context: LayoutContext) {
122
128
  (group) =>
123
129
  axisTextMetrics(
124
130
  group.domain,
125
- true,
131
+ props.axes?.y?.nice !== false,
126
132
  undefined,
127
133
  group.seriesList[0]?.unit,
128
134
  props.axes?.y?.splitNumber,
@@ -173,6 +179,7 @@ export function useWaveformLayout(context: LayoutContext) {
173
179
  props.yDomain,
174
180
  props.yDomains,
175
181
  props.axes?.y?.splitNumber,
182
+ props.axes?.y?.nice !== false,
176
183
  )
177
184
  return {
178
185
  left: Math.max(maximum.left, clearance.left),
@@ -223,9 +230,7 @@ export function useWaveformLayout(context: LayoutContext) {
223
230
  const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
224
231
  const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit})`)
225
232
  const activeInteractionMode = computed(() => props.interactionMode)
226
- const isZoomMode = computed(
227
- () => activeInteractionMode.value === 'zoom' || activeInteractionMode.value === undefined,
228
- )
233
+ const isZoomMode = computed(() => activeInteractionMode.value !== 'annotation')
229
234
  const sharedXDomain = computed(() => {
230
235
  const values: number[] = []
231
236
  chartTracks.value.forEach((track) => {
@@ -285,10 +290,10 @@ export function useWaveformLayout(context: LayoutContext) {
285
290
  innerHeight.value,
286
291
  gridOptions.value,
287
292
  props.displayMode,
288
- pagedTracks.value.map(Boolean),
293
+ layoutTracks.value.map(Boolean),
289
294
  yAxisLayout.value.horizontalGap,
290
295
  )
291
- return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] }))
296
+ return cells.map((cell, index) => ({ ...cell, series: layoutTracks.value[index] }))
292
297
  })
293
298
  const trackLayouts = computed<TrackLayout[]>(() =>
294
299
  buildTrackLayouts({
@@ -315,6 +320,7 @@ export function useWaveformLayout(context: LayoutContext) {
315
320
  timeUnit: props.timeUnit,
316
321
  xAxisLabelFormatter: props.axes?.x?.labelFormatter,
317
322
  yAxisSplitNumber: props.axes?.y?.splitNumber,
323
+ yAxisNice: props.axes?.y?.nice,
318
324
  rendering: renderingOptions.value,
319
325
  hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
320
326
  yAxisLabelX: yAxisMetrics.value.labelCenterX,
@@ -0,0 +1,174 @@
1
+ import { zoomIdentity } from 'd3'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
+ import type { WaveformData } from '../../types'
6
+ import { buildTrackLayouts } from './trackLayoutBuilder'
7
+ import type { DisplaySeries, DisplayTrack } from './types'
8
+ import { prepareWaveformSeries } from './useWaveformData'
9
+
10
+ function displaySeries(id: string, domain: [number, number]): DisplaySeries {
11
+ return {
12
+ id,
13
+ name: id,
14
+ color: '#1677ff',
15
+ lineType: 'linear',
16
+ lineStyle: 'solid',
17
+ pointType: 'none',
18
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
19
+ points: [
20
+ { x: 0, y: domain[0] },
21
+ { x: 1, y: domain[1] },
22
+ ],
23
+ xDomain: [0, 1],
24
+ yDomain: domain,
25
+ hasErrorPoints: false,
26
+ }
27
+ }
28
+
29
+ function layout(
30
+ series: DisplaySeries[],
31
+ options: { overlayMode?: 'single-axis' | 'multi-axis'; fixedYDomain?: [number, number] } = {},
32
+ ) {
33
+ const track: DisplayTrack = {
34
+ id: 'track',
35
+ series,
36
+ visibleSeries: series,
37
+ xDomain: [0, 1],
38
+ yDomain: [
39
+ Math.min(...series.map((item) => item.yDomain[0])),
40
+ Math.max(...series.map((item) => item.yDomain[1])),
41
+ ],
42
+ }
43
+ return buildTrackLayouts({
44
+ cells: [
45
+ {
46
+ slotIndex: 0,
47
+ row: 0,
48
+ column: 0,
49
+ left: 0,
50
+ top: 0,
51
+ width: 120,
52
+ height: 100,
53
+ plotHeight: 100,
54
+ cellHeight: 130,
55
+ xAxisBand: 30,
56
+ series: track,
57
+ },
58
+ ],
59
+ grid: {
60
+ rowCount: 1,
61
+ columnCount: 1,
62
+ showPagination: false,
63
+ fillIncompleteLastRow: false,
64
+ trackLines: {},
65
+ },
66
+ displayMode: 'independent',
67
+ overlayMode: options.overlayMode ?? 'single-axis',
68
+ independentTransforms: [zoomIdentity],
69
+ sharedZoomDomain: [0, 1],
70
+ fixedYDomain: options.fixedYDomain,
71
+ timeUnit: 'ms',
72
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
73
+ hideSecondaryLabels: false,
74
+ yAxisLabelX: -50,
75
+ yAxisNice: false,
76
+ showCompactEmptyTracks: false,
77
+ })[0]!
78
+ }
79
+
80
+ describe('Y-axis nice domains', () => {
81
+ it('defaults to a nice domain and can preserve the actual data endpoints', () => {
82
+ const source = displaySeries('source', [3, 97])
83
+ const track: DisplayTrack = {
84
+ id: 'track',
85
+ series: [source],
86
+ visibleSeries: [source],
87
+ xDomain: [0, 1],
88
+ yDomain: [3, 97],
89
+ }
90
+ const defaults = buildTrackLayouts({
91
+ cells: [
92
+ {
93
+ slotIndex: 0,
94
+ row: 0,
95
+ column: 0,
96
+ left: 0,
97
+ top: 0,
98
+ width: 120,
99
+ height: 100,
100
+ plotHeight: 100,
101
+ cellHeight: 130,
102
+ xAxisBand: 30,
103
+ series: track,
104
+ },
105
+ ],
106
+ grid: {
107
+ rowCount: 1,
108
+ columnCount: 1,
109
+ showPagination: false,
110
+ fillIncompleteLastRow: false,
111
+ trackLines: {},
112
+ },
113
+ displayMode: 'independent',
114
+ overlayMode: 'single-axis',
115
+ independentTransforms: [zoomIdentity],
116
+ sharedZoomDomain: [0, 1],
117
+ timeUnit: 'ms',
118
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
119
+ hideSecondaryLabels: false,
120
+ yAxisLabelX: -50,
121
+ showCompactEmptyTracks: false,
122
+ })[0]!
123
+
124
+ expect(defaults.yScale.domain()).toEqual([0, 100])
125
+ expect(layout([source]).yScale.domain()).toEqual([3, 97])
126
+ })
127
+
128
+ it('merges visible single-axis series but keeps multi-axis series independent', () => {
129
+ const first = displaySeries('first', [3, 40])
130
+ const second = displaySeries('second', [10, 97])
131
+
132
+ expect(layout([first, second]).yScale.domain()).toEqual([3, 97])
133
+ expect(
134
+ layout([first, second], { overlayMode: 'multi-axis' }).yAxes.map((axis) =>
135
+ axis.scale.domain(),
136
+ ),
137
+ ).toEqual([
138
+ [3, 40],
139
+ [10, 97],
140
+ ])
141
+ })
142
+
143
+ it('keeps explicit domains and constant-data safety ranges unchanged', () => {
144
+ expect(
145
+ layout([displaySeries('source', [3, 97])], { fixedYDomain: [3, 97] }).yScale.domain(),
146
+ ).toEqual([3, 97])
147
+ expect(layout([displaySeries('constant', [9.5, 10.5])]).yScale.domain()).toEqual([9.5, 10.5])
148
+ })
149
+
150
+ it('continues to include visible error-bar bounds in the preserved domain', () => {
151
+ const input: WaveformData = {
152
+ kind: 'series',
153
+ series: [
154
+ {
155
+ id: 'error-series',
156
+ name: 'error series',
157
+ errorBar: { visible: true },
158
+ data: {
159
+ kind: 'points',
160
+ points: [
161
+ { x: 0, y: 3, lowerError: 2 },
162
+ { x: 1, y: 7, upperError: 4 },
163
+ ],
164
+ },
165
+ },
166
+ ],
167
+ }
168
+ const [prepared] = prepareWaveformSeries(input)
169
+ const errorSeries: DisplaySeries = { ...prepared!, color: prepared!.color ?? '#1677ff' }
170
+
171
+ expect(prepared?.yDomain).toEqual([1, 11])
172
+ expect(layout([errorSeries]).yScale.domain()).toEqual([1, 11])
173
+ })
174
+ })