waveform-analysis 0.1.29 → 0.1.30

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.
Files changed (35) hide show
  1. package/dist/index.cjs +1 -1
  2. package/dist/index.js +312 -303
  3. package/dist/types/components/WaveformChart.vue.d.ts +1 -0
  4. package/dist/types/components/core/constants.d.ts +2 -0
  5. package/dist/types/components/core/useWaveformChartController.d.ts +6 -1
  6. package/dist/types/components/core/useWaveformLayout.d.ts +0 -1
  7. package/dist/types/components/core/useWaveformPresentation.d.ts +5 -0
  8. package/dist/types/components/core/waveformChartTypes.d.ts +3 -2
  9. package/dist/types/components/data/types.d.ts +1 -1
  10. package/dist/types/components/index.d.ts +1 -1
  11. package/dist/types/components/waveform.d.ts +1 -1
  12. package/dist/types/demo/types.d.ts +4 -1
  13. package/dist/types/index.d.ts +1 -1
  14. package/dist/types/types/chart.d.ts +7 -0
  15. package/dist/types/types/index.d.ts +1 -1
  16. package/package.json +1 -1
  17. package/src/App.vue +10 -0
  18. package/src/components/WaveformChart.vue +1 -0
  19. package/src/components/WaveformChartView.vue +13 -10
  20. package/src/components/core/constants.ts +3 -0
  21. package/src/components/core/useWaveformLayout.ts +0 -3
  22. package/src/components/core/useWaveformPresentation.ts +17 -2
  23. package/src/components/core/waveformChartTypes.ts +3 -0
  24. package/src/components/data/types.ts +1 -0
  25. package/src/components/index.ts +1 -0
  26. package/src/components/waveform.ts +1 -0
  27. package/src/components/waveformChartCases/sizingAndTitles.test.ts +59 -0
  28. package/src/demo/DemoChartHost.vue +1 -0
  29. package/src/demo/DemoGridFrameControls.vue +29 -0
  30. package/src/demo/plotMarginControls.test.ts +29 -0
  31. package/src/demo/types.ts +4 -0
  32. package/src/index.ts +1 -0
  33. package/src/styles.css +1 -0
  34. package/src/types/chart.ts +8 -0
  35. package/src/types/index.ts +1 -0
@@ -48,6 +48,7 @@ declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
48
48
  annotationsVisible: boolean;
49
49
  grid: import("..").WaveformGridOptions;
50
50
  rendering: import("..").WaveformRenderingOptions;
51
+ plotMargin: import("..").WaveformPlotMargin;
51
52
  defaultHiddenSeriesIds: string[];
52
53
  cleanView: boolean;
53
54
  presentationMode: boolean;
@@ -8,6 +8,8 @@ export declare const margin: {
8
8
  bottom: number;
9
9
  left: number;
10
10
  };
11
+ /** Distance from the drawing SVG bottom edge to the X-axis title baseline. */
12
+ export declare const X_AXIS_TITLE_BOTTOM_OFFSET = 12;
11
13
  /**
12
14
  * 图表最小高度(像素)
13
15
  */
@@ -287,7 +287,6 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
287
287
  resolveSeriesYScale: (track: import("./types").TrackLayout, seriesId: string) => import("d3").ScaleLinear<number, number, never>;
288
288
  annotationTrackLayouts: import("../annotation").AnnotationTrackLayout[];
289
289
  renderedAnnotations: import("../annotation").RenderedAnnotation[];
290
- xAxisTitleY: number;
291
290
  editorSeries: import("../annotation").AnnotationSeriesInfo | undefined;
292
291
  resolveFrameNumber: (trackIndex: number) => string | number | undefined;
293
292
  fixedWidth: number | undefined;
@@ -318,8 +317,13 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
318
317
  titleMeasureStyle: import("vue").CSSProperties;
319
318
  titleLayout: import("./title").RotatedTitleLayout;
320
319
  titleAreaHeight: number;
320
+ resolvedPlotMargin: {
321
+ top: number;
322
+ bottom: number;
323
+ };
321
324
  chartTopMargin: number;
322
325
  drawingHeight: number;
326
+ xAxisTitleY: number;
323
327
  innerHeight: number;
324
328
  titleAreaStyle: import("vue").CSSProperties;
325
329
  titleVisualStyle: import("vue").CSSProperties;
@@ -349,6 +353,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
349
353
  interactionMode?: import("..").WaveformInteractionMode | undefined;
350
354
  grid: import("./grid").WaveformGridOptions;
351
355
  rendering: import("..").WaveformRenderingOptions;
356
+ plotMargin: import("..").WaveformPlotMargin;
352
357
  title?: import("..").WaveformTitleOptions | undefined;
353
358
  legend: import("..").WaveformLegendOptions;
354
359
  hiddenSeriesIds?: string[] | undefined;
@@ -54,7 +54,6 @@ export declare function useWaveformLayout(context: LayoutContext): {
54
54
  resolveSeriesYScale: (track: TrackLayout, seriesId: string) => import("d3").ScaleLinear<number, number, never>;
55
55
  annotationTrackLayouts: ComputedRef<AnnotationTrackLayout[]>;
56
56
  renderedAnnotations: ComputedRef<import("../annotation").RenderedAnnotation[]>;
57
- xAxisTitleY: ComputedRef<number>;
58
57
  editorSeries: ComputedRef<AnnotationSeriesInfo | undefined>;
59
58
  resolveFrameNumber: (trackIndex: number) => string | number | undefined;
60
59
  };
@@ -39,8 +39,13 @@ export declare function useWaveformPresentation(context: PresentationContext): {
39
39
  titleMeasureStyle: import("vue").ComputedRef<CSSProperties>;
40
40
  titleLayout: import("vue").ComputedRef<import("./title").RotatedTitleLayout>;
41
41
  titleAreaHeight: import("vue").ComputedRef<number>;
42
+ resolvedPlotMargin: import("vue").ComputedRef<{
43
+ top: number;
44
+ bottom: number;
45
+ }>;
42
46
  chartTopMargin: import("vue").ComputedRef<number>;
43
47
  drawingHeight: import("vue").ComputedRef<number>;
48
+ xAxisTitleY: import("vue").ComputedRef<number>;
44
49
  innerHeight: import("vue").ComputedRef<number>;
45
50
  titleAreaStyle: import("vue").ComputedRef<CSSProperties>;
46
51
  titleVisualStyle: import("vue").ComputedRef<CSSProperties>;
@@ -1,4 +1,4 @@
1
- import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformInteractionMode, WaveformLegendOptions, WaveformOverlayMode, WaveformPoint, WaveformRenderingOptions, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../data/types';
1
+ import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformInteractionMode, WaveformLegendOptions, WaveformOverlayMode, WaveformPlotMargin, WaveformPoint, WaveformRenderingOptions, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../data/types';
2
2
  import type { WaveformGridOptions } from './grid';
3
3
  export interface WaveformChartProps {
4
4
  data: WaveformData;
@@ -27,6 +27,7 @@ export interface WaveformChartProps {
27
27
  interactionMode?: WaveformInteractionMode;
28
28
  grid?: WaveformGridOptions;
29
29
  rendering?: WaveformRenderingOptions;
30
+ plotMargin?: WaveformPlotMargin;
30
31
  title?: WaveformTitleOptions;
31
32
  legend?: WaveformLegendOptions;
32
33
  hiddenSeriesIds?: string[];
@@ -35,7 +36,7 @@ export interface WaveformChartProps {
35
36
  presentationMode?: boolean;
36
37
  zeroLine?: WaveformZeroLineOptions;
37
38
  }
38
- type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'pannable' | 'minVisiblePoints' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
39
+ type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'pannable' | 'minVisiblePoints' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'plotMargin' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
39
40
  export type ResolvedWaveformChartProps = Readonly<WaveformChartProps & Required<Pick<WaveformChartProps, DefaultedProp>>>;
40
41
  export interface WaveformChartEmit {
41
42
  (event: 'point-hover', point: WaveformPoint | null): void;
@@ -2,6 +2,6 @@
2
2
  * 数据系统类型定义
3
3
  * 重新导出 src/types 中的类型,方便组件内部使用
4
4
  */
5
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from '../../types';
5
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from '../../types';
6
6
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from '../core/grid';
7
7
  export { normalizeWaveformData, normalizeWaveformSeries } from '../../core';
@@ -1,5 +1,5 @@
1
1
  export { default as WaveformChart } from './WaveformChart.vue';
2
- export type { SingleWaveformData, WaveformData, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, WaveformPoint, WaveformSeries, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './data/types';
2
+ export type { SingleWaveformData, WaveformData, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformAxesOptions, WaveformZeroLineOptions, WaveformPoint, WaveformSeries, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './data/types';
3
3
  export type { WaveformGridOptions as WaveformGridConfig } from './core/grid';
4
4
  export { WaveformTooltip } from './interaction';
5
5
  export { WaveformTrack } from './rendering';
@@ -2,5 +2,5 @@
2
2
  * 向后兼容的导出文件
3
3
  * 保留原有导入路径,内部从新的模块结构导出
4
4
  */
5
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformFrameStyle, SingleWaveformData, WaveformSeries, WaveformLineStyle, WaveformData, NormalizedWaveformSeries, } from '../types';
5
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformFrameStyle, SingleWaveformData, WaveformSeries, WaveformLineStyle, WaveformData, NormalizedWaveformSeries, } from '../types';
6
6
  export { normalizeWaveformData, normalizeWaveformSeries } from '../core';
@@ -1,4 +1,4 @@
1
- import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
1
+ import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformPlotMargin, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
2
2
  import type { DemoXAxisLabelFormat, DemoXAxisLabelTimeZone } from './useDemoXAxisLabelControls';
3
3
  interface SelectOption<T> {
4
4
  label: string;
@@ -9,6 +9,8 @@ export interface DemoControlPanelModel {
9
9
  displayMode: WaveformDisplayMode;
10
10
  overlayMode: WaveformOverlayMode;
11
11
  showTooltip: boolean;
12
+ plotMarginTop: number;
13
+ plotMarginBottom: number;
12
14
  cleanView: boolean;
13
15
  presentationMode: boolean;
14
16
  selectedSeriesId: string;
@@ -81,6 +83,7 @@ export interface DemoChartModel {
81
83
  cleanView: boolean;
82
84
  presentationMode: boolean;
83
85
  showTooltip: boolean;
86
+ plotMargin: WaveformPlotMargin;
84
87
  zeroLine: WaveformZeroLineOptions;
85
88
  frameWatermarkVisible: boolean;
86
89
  annotations: WaveformAnnotation[];
@@ -3,7 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as WaveformChart } from './components/WaveformChart.vue';
6
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
6
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
7
7
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './components/core/grid';
8
8
  export { normalizeWaveformData, normalizeWaveformSeries } from './core';
9
9
  export { paddedDomain, buildMinorTicks, displayTime, formatEndpointTime, formatAxisTime, formatTooltipTime, resolveTrackGeometry, clamp, type TimeUnit, type TrackGeometry, } from './utils';
@@ -66,6 +66,13 @@ export interface WaveformRenderingOptions {
66
66
  /** Minimum horizontal CSS-pixel spacing between rendered error bars. Use 0 to disable. */
67
67
  errorBarMinSpacing?: number;
68
68
  }
69
+ /** Pixel margins reserved above and below the waveform plotting area. */
70
+ export interface WaveformPlotMargin {
71
+ /** Space between the top of the drawing SVG and the plotting area. Defaults to 18. */
72
+ top?: number;
73
+ /** Space between the plotting area and the bottom of the drawing SVG. Defaults to 52. */
74
+ bottom?: number;
75
+ }
69
76
  /** Text styling for the chart-level title. */
70
77
  export interface WaveformTitleTextStyle {
71
78
  color?: string;
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * 类型定义统一导出
3
3
  */
4
- export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
4
+ export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformZoomEndPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, } from './chart';
5
5
  export type { SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './data';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
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
@@ -14,6 +14,7 @@ import {
14
14
  type WaveformLegendOrientation,
15
15
  type WaveformLegendPosition,
16
16
  type WaveformOverlayMode,
17
+ type WaveformPlotMargin,
17
18
  type WaveformTitleOptions,
18
19
  type WaveformZoomEndPayload,
19
20
  type WaveformZeroLineOptions,
@@ -47,6 +48,8 @@ const annotationsVisible = ref(true)
47
48
  const cleanView = ref(false)
48
49
  const presentationMode = ref(false)
49
50
  const showTooltip = ref(true)
51
+ const plotMarginTop = ref(18)
52
+ const plotMarginBottom = ref(52)
50
53
  const zeroLineVisible = ref(false)
51
54
  const zeroLineColor = ref('#98a2b3')
52
55
  const zeroLineWidth = ref(1)
@@ -126,6 +129,10 @@ const zeroLine = computed<WaveformZeroLineOptions>(() => ({
126
129
  width: zeroLineWidth.value,
127
130
  dash: zeroLineDash.value,
128
131
  }))
132
+ const plotMargin = computed<WaveformPlotMargin>(() => ({
133
+ top: plotMarginTop.value,
134
+ bottom: plotMarginBottom.value,
135
+ }))
129
136
 
130
137
  const initialXValues = normalizeWaveformSeries(fullChartData).flatMap((series) =>
131
138
  series.points.map((point) => point.x),
@@ -294,6 +301,8 @@ const controlPanelModel = reactive({
294
301
  displayMode,
295
302
  overlayMode,
296
303
  showTooltip,
304
+ plotMarginTop,
305
+ plotMarginBottom,
297
306
  cleanView,
298
307
  presentationMode,
299
308
  selectedSeriesId,
@@ -360,6 +369,7 @@ const chartModel = reactive({
360
369
  cleanView,
361
370
  presentationMode,
362
371
  showTooltip,
372
+ plotMargin,
363
373
  zeroLine,
364
374
  frameWatermarkVisible,
365
375
  annotations,
@@ -23,6 +23,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
23
23
  interactionMode: undefined,
24
24
  grid: () => ({ rowCount: 2, columnCount: 1, showPagination: true }),
25
25
  rendering: () => ({}),
26
+ plotMargin: () => ({}),
26
27
  legend: () => ({ position: 'top-right', orientation: 'auto' }),
27
28
  defaultHiddenSeriesIds: () => [],
28
29
  cleanView: false,
@@ -19,6 +19,7 @@ const {
19
19
  overlayMode,
20
20
  resolvedChartLeftMargin,
21
21
  titleAreaHeight,
22
+ resolvedPlotMargin,
22
23
  pointerInsideChart,
23
24
  handleNativeContextMenu,
24
25
  titleAreaReserved,
@@ -118,6 +119,8 @@ const {
118
119
  :data-presentation-mode="isPresentationMode"
119
120
  :data-overlay-mode="overlayMode"
120
121
  :data-chart-left-margin="resolvedChartLeftMargin"
122
+ :data-plot-margin-top="resolvedPlotMargin.top"
123
+ :data-plot-margin-bottom="resolvedPlotMargin.bottom"
121
124
  :data-title-area-height="titleAreaHeight"
122
125
  @pointerenter="pointerInsideChart = true"
123
126
  @pointerleave="pointerInsideChart = false"
@@ -287,18 +290,18 @@ const {
287
290
  />
288
291
  </g>
289
292
  </g>
290
-
291
- <text
292
- v-if="resolvedXLabel && !isCleanView"
293
- class="waveform-chart__label waveform-chart__x-label"
294
- :x="innerWidth / 2"
295
- :y="xAxisTitleY"
296
- text-anchor="middle"
297
- >
298
- {{ resolvedXLabel }}
299
- </text>
300
293
  </g>
301
294
 
295
+ <text
296
+ v-if="resolvedXLabel && !isCleanView"
297
+ class="waveform-chart__label waveform-chart__x-label"
298
+ :x="resolvedChartLeftMargin + innerWidth / 2"
299
+ :y="xAxisTitleY"
300
+ text-anchor="middle"
301
+ >
302
+ {{ resolvedXLabel }}
303
+ </text>
304
+
302
305
  <text
303
306
  v-if="hasChartArea && !hasWaveformData"
304
307
  class="waveform-chart__empty"
@@ -7,6 +7,9 @@
7
7
  /** 图表边距 */
8
8
  export const margin = { top: 18, right: 24, bottom: 52, left: 64 }
9
9
 
10
+ /** Distance from the drawing SVG bottom edge to the X-axis title baseline. */
11
+ export const X_AXIS_TITLE_BOTTOM_OFFSET = 12
12
+
10
13
  /**
11
14
  * 图表最小高度(像素)
12
15
  */
@@ -24,7 +24,6 @@ import {
24
24
  normalizeGridOptions,
25
25
  paginateSeries,
26
26
  resolveGridCellGeometry,
27
- X_AXIS_BAND,
28
27
  } from './grid'
29
28
  import {
30
29
  buildTrackLayouts,
@@ -316,7 +315,6 @@ export function useWaveformLayout(context: LayoutContext) {
316
315
  )
317
316
  : [],
318
317
  )
319
- const xAxisTitleY = computed(() => innerHeight.value + X_AXIS_BAND + 10)
320
318
  const editorSeries = computed<AnnotationSeriesInfo | undefined>(() => {
321
319
  const seriesId = annotationInteraction.editorDraft.value?.annotation.seriesId
322
320
  const series = chartSeries.value.find((item) => item.id === seriesId)
@@ -360,7 +358,6 @@ export function useWaveformLayout(context: LayoutContext) {
360
358
  resolveSeriesYScale,
361
359
  annotationTrackLayouts,
362
360
  renderedAnnotations,
363
- xAxisTitleY,
364
361
  editorSeries,
365
362
  resolveFrameNumber,
366
363
  }
@@ -7,6 +7,7 @@ import {
7
7
  TITLE_CHAR_WIDTH_RATIO,
8
8
  TITLE_DEFAULT_FONT_SIZE,
9
9
  TITLE_LINE_HEIGHT,
10
+ X_AXIS_TITLE_BOTTOM_OFFSET,
10
11
  ZERO_LINE_DEFAULTS,
11
12
  } from './constants'
12
13
  import { calculateRotatedTitleLayout, TITLE_AREA_HORIZONTAL_PADDING } from './title'
@@ -144,11 +145,23 @@ export function useWaveformPresentation(context: PresentationContext) {
144
145
  const titleAreaHeight = computed(() =>
145
146
  titleAreaReserved.value ? titleLayout.value.areaHeight : 0,
146
147
  )
147
- const chartTopMargin = computed(() => margin.top)
148
+ const resolvePlotMargin = (value: number | undefined, fallback: number) =>
149
+ typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : fallback
150
+ const resolvedPlotMargin = computed(() => ({
151
+ top: resolvePlotMargin(props.plotMargin.top, margin.top),
152
+ bottom: resolvePlotMargin(props.plotMargin.bottom, margin.bottom),
153
+ }))
154
+ const chartTopMargin = computed(() => resolvedPlotMargin.value.top)
148
155
  const drawingHeight = computed(() =>
149
156
  Math.max(0, chartHeight.value - titleAreaHeight.value - paginationBandHeight.value),
150
157
  )
151
- const innerHeight = computed(() => Math.max(0, drawingHeight.value - margin.top - margin.bottom))
158
+ const xAxisTitleY = computed(() => Math.max(0, drawingHeight.value - X_AXIS_TITLE_BOTTOM_OFFSET))
159
+ const innerHeight = computed(() =>
160
+ Math.max(
161
+ 0,
162
+ drawingHeight.value - resolvedPlotMargin.value.top - resolvedPlotMargin.value.bottom,
163
+ ),
164
+ )
152
165
  const titleAreaStyle = computed<CSSProperties>(() => ({
153
166
  height: `${titleAreaHeight.value}px`,
154
167
  justifyContent:
@@ -193,8 +206,10 @@ export function useWaveformPresentation(context: PresentationContext) {
193
206
  titleMeasureStyle,
194
207
  titleLayout,
195
208
  titleAreaHeight,
209
+ resolvedPlotMargin,
196
210
  chartTopMargin,
197
211
  drawingHeight,
212
+ xAxisTitleY,
198
213
  innerHeight,
199
214
  titleAreaStyle,
200
215
  titleVisualStyle,
@@ -7,6 +7,7 @@ import type {
7
7
  WaveformInteractionMode,
8
8
  WaveformLegendOptions,
9
9
  WaveformOverlayMode,
10
+ WaveformPlotMargin,
10
11
  WaveformPoint,
11
12
  WaveformRenderingOptions,
12
13
  WaveformTitleOptions,
@@ -42,6 +43,7 @@ export interface WaveformChartProps {
42
43
  interactionMode?: WaveformInteractionMode
43
44
  grid?: WaveformGridOptions
44
45
  rendering?: WaveformRenderingOptions
46
+ plotMargin?: WaveformPlotMargin
45
47
  title?: WaveformTitleOptions
46
48
  legend?: WaveformLegendOptions
47
49
  hiddenSeriesIds?: string[]
@@ -65,6 +67,7 @@ type DefaultedProp =
65
67
  | 'annotationsVisible'
66
68
  | 'grid'
67
69
  | 'rendering'
70
+ | 'plotMargin'
68
71
  | 'legend'
69
72
  | 'defaultHiddenSeriesIds'
70
73
  | 'cleanView'
@@ -13,6 +13,7 @@ export type {
13
13
  WaveformAnnotationStyle,
14
14
  WaveformAnnotation,
15
15
  WaveformRenderingOptions,
16
+ WaveformPlotMargin,
16
17
  WaveformTitleTextStyle,
17
18
  WaveformTitleOptions,
18
19
  WaveformLegendPosition,
@@ -11,6 +11,7 @@ export type {
11
11
  WaveformAnnotationStyle,
12
12
  WaveformAnnotation,
13
13
  WaveformRenderingOptions,
14
+ WaveformPlotMargin,
14
15
  WaveformTitleTextStyle,
15
16
  WaveformTitleOptions,
16
17
  WaveformLegendPosition,
@@ -12,6 +12,7 @@ export type {
12
12
  WaveformAnnotationStyle,
13
13
  WaveformAnnotation,
14
14
  WaveformRenderingOptions,
15
+ WaveformPlotMargin,
15
16
  WaveformTitleTextStyle,
16
17
  WaveformTitleOptions,
17
18
  WaveformFrameStyle,
@@ -105,6 +105,65 @@ describe('WaveformChart', () => {
105
105
  expect(wrapper.get('.waveform-chart__svg').attributes('height')).toBe('300')
106
106
  })
107
107
 
108
+ it('configures plot top and bottom margins and reacts to updates', async () => {
109
+ const wrapper = await mountSizedChart(
110
+ { kind: 'samples', values: [0, 1], sampleRate: 1 },
111
+ {
112
+ displayMode: 'compact',
113
+ grid: { rowCount: 1, columnCount: 1 },
114
+ plotMargin: { top: 30, bottom: 70 },
115
+ },
116
+ )
117
+
118
+ expect(wrapper.attributes('data-plot-margin-top')).toBe('30')
119
+ expect(wrapper.attributes('data-plot-margin-bottom')).toBe('70')
120
+ expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('260')
121
+ expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 30)')
122
+
123
+ await wrapper.setProps({ plotMargin: { top: 12 } })
124
+
125
+ expect(wrapper.attributes('data-plot-margin-top')).toBe('12')
126
+ expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
127
+ expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('296')
128
+ })
129
+
130
+ it('falls back to default plot margins for invalid values', async () => {
131
+ const wrapper = await mountSizedChart(
132
+ { kind: 'samples', values: [0, 1], sampleRate: 1 },
133
+ {
134
+ displayMode: 'compact',
135
+ grid: { rowCount: 1, columnCount: 1 },
136
+ plotMargin: { top: -1, bottom: Number.NaN },
137
+ },
138
+ )
139
+
140
+ expect(wrapper.attributes('data-plot-margin-top')).toBe('18')
141
+ expect(wrapper.attributes('data-plot-margin-bottom')).toBe('52')
142
+ expect(wrapper.get('.waveform-chart__overlay').attributes('height')).toBe('290')
143
+ })
144
+
145
+ it('keeps the chart title and X-axis title fixed when plot margins change', async () => {
146
+ const wrapper = await mountSizedChart(
147
+ { kind: 'samples', values: [0, 1], sampleRate: 1 },
148
+ {
149
+ displayMode: 'compact',
150
+ grid: { rowCount: 1, columnCount: 1 },
151
+ title: { text: '固定标题' },
152
+ },
153
+ )
154
+ const titleAreaStyle = wrapper.get('.waveform-chart__title-area').attributes('style')
155
+ const xAxisTitle = wrapper.get('.waveform-chart__x-label')
156
+ const initialX = xAxisTitle.attributes('x')
157
+ const initialY = xAxisTitle.attributes('y')
158
+
159
+ await wrapper.setProps({ plotMargin: { top: 60, bottom: 90 } })
160
+
161
+ expect(wrapper.get('.waveform-chart__title-area').attributes('style')).toBe(titleAreaStyle)
162
+ expect(wrapper.get('.waveform-chart__x-label').attributes('x')).toBe(initialX)
163
+ expect(wrapper.get('.waveform-chart__x-label').attributes('y')).toBe(initialY)
164
+ expect(wrapper.get('.waveform-chart__svg > g').attributes('transform')).toContain(', 60)')
165
+ })
166
+
108
167
  it('does not render or reserve space for missing, hidden, or blank titles', async () => {
109
168
  for (const title of [undefined, { visible: false, text: '隐藏标题' }, { text: ' ' }]) {
110
169
  const wrapper = await mountSizedChart(
@@ -45,6 +45,7 @@ defineExpose({ resetViewport })
45
45
  :clean-view="model.cleanView"
46
46
  :presentation-mode="model.presentationMode"
47
47
  :show-tooltip="model.showTooltip"
48
+ :plot-margin="model.plotMargin"
48
49
  :zero-line="model.zeroLine"
49
50
  :frame-number="model.frameWatermarkVisible ? 1 : undefined"
50
51
  :annotations-visible="model.annotationsVisible"
@@ -8,6 +8,35 @@ const model = defineModel<DemoControlPanelModel>('model', { required: true })
8
8
  </script>
9
9
 
10
10
  <template>
11
+ <section class="control-section">
12
+ <h2>绘图区边距</h2>
13
+ <div class="plot-margin-controls">
14
+ <label class="frame-style-control">
15
+ <span>上边距</span>
16
+ <InputNumber
17
+ v-model:value="model.plotMarginTop"
18
+ :min="0"
19
+ :max="300"
20
+ :step="1"
21
+ addon-after="px"
22
+ size="small"
23
+ aria-label="绘图区上边距"
24
+ />
25
+ </label>
26
+ <label class="frame-style-control">
27
+ <span>下边距</span>
28
+ <InputNumber
29
+ v-model:value="model.plotMarginBottom"
30
+ :min="0"
31
+ :max="300"
32
+ :step="1"
33
+ addon-after="px"
34
+ size="small"
35
+ aria-label="绘图区下边距"
36
+ />
37
+ </label>
38
+ </div>
39
+ </section>
11
40
  <section class="control-section">
12
41
  <div class="control-section__header">
13
42
  <h2>零值参考线</h2>
@@ -0,0 +1,29 @@
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
+ import { InputNumber } from 'ant-design-vue'
3
+ import { describe, expect, it } from 'vitest'
4
+
5
+ import App from '../App.vue'
6
+ import { WaveformChart } from '../components'
7
+
8
+ describe('plot margin demo controls', () => {
9
+ it('updates the chart top and bottom margins from the sidebar', async () => {
10
+ const wrapper = mount(App)
11
+ await flushPromises()
12
+ const chart = wrapper.getComponent(WaveformChart)
13
+ const controls = wrapper.get('.plot-margin-controls').findAllComponents(InputNumber)
14
+
15
+ expect(controls).toHaveLength(2)
16
+ expect(controls[0]?.props('value')).toBe(18)
17
+ expect(controls[1]?.props('value')).toBe(52)
18
+ expect(chart.props('plotMargin')).toEqual({ top: 18, bottom: 52 })
19
+
20
+ controls[0]?.vm.$emit('update:value', 30)
21
+ controls[1]?.vm.$emit('update:value', 70)
22
+ await flushPromises()
23
+
24
+ expect(chart.props('plotMargin')).toEqual({ top: 30, bottom: 70 })
25
+ expect(wrapper.get('.waveform-chart').attributes('data-plot-margin-top')).toBe('30')
26
+ expect(wrapper.get('.waveform-chart').attributes('data-plot-margin-bottom')).toBe('70')
27
+ wrapper.unmount()
28
+ })
29
+ })
package/src/demo/types.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  WaveformLegendPosition,
11
11
  WaveformLineStyle,
12
12
  WaveformOverlayMode,
13
+ WaveformPlotMargin,
13
14
  WaveformTitleOptions,
14
15
  WaveformZeroLineOptions,
15
16
  WaveformZoomEndPayload,
@@ -26,6 +27,8 @@ export interface DemoControlPanelModel {
26
27
  displayMode: WaveformDisplayMode
27
28
  overlayMode: WaveformOverlayMode
28
29
  showTooltip: boolean
30
+ plotMarginTop: number
31
+ plotMarginBottom: number
29
32
  cleanView: boolean
30
33
  presentationMode: boolean
31
34
  selectedSeriesId: string
@@ -99,6 +102,7 @@ export interface DemoChartModel {
99
102
  cleanView: boolean
100
103
  presentationMode: boolean
101
104
  showTooltip: boolean
105
+ plotMargin: WaveformPlotMargin
102
106
  zeroLine: WaveformZeroLineOptions
103
107
  frameWatermarkVisible: boolean
104
108
  annotations: WaveformAnnotation[]
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@ export type {
17
17
  WaveformAnnotationStyle,
18
18
  WaveformAnnotation,
19
19
  WaveformRenderingOptions,
20
+ WaveformPlotMargin,
20
21
  WaveformTitleTextStyle,
21
22
  WaveformTitleOptions,
22
23
  WaveformLegendPosition,
package/src/styles.css CHANGED
@@ -195,6 +195,7 @@ body {
195
195
  }
196
196
 
197
197
  .frame-style-controls,
198
+ .plot-margin-controls,
198
199
  .auxiliary-style-controls {
199
200
  display: grid;
200
201
  gap: 10px;
@@ -74,6 +74,14 @@ export interface WaveformRenderingOptions {
74
74
  errorBarMinSpacing?: number
75
75
  }
76
76
 
77
+ /** Pixel margins reserved above and below the waveform plotting area. */
78
+ export interface WaveformPlotMargin {
79
+ /** Space between the top of the drawing SVG and the plotting area. Defaults to 18. */
80
+ top?: number
81
+ /** Space between the plotting area and the bottom of the drawing SVG. Defaults to 52. */
82
+ bottom?: number
83
+ }
84
+
77
85
  /** Text styling for the chart-level title. */
78
86
  export interface WaveformTitleTextStyle {
79
87
  color?: string
@@ -12,6 +12,7 @@ export type {
12
12
  WaveformAnnotationStyle,
13
13
  WaveformAnnotation,
14
14
  WaveformRenderingOptions,
15
+ WaveformPlotMargin,
15
16
  WaveformTitleTextStyle,
16
17
  WaveformTitleOptions,
17
18
  WaveformLegendPosition,