waveform-analysis 0.1.65 → 0.1.66

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 (47) hide show
  1. package/README.md +23 -3
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +705 -617
  4. package/dist/types/components/WaveformChart.vue.d.ts +3 -2
  5. package/dist/types/components/core/constants.d.ts +2 -0
  6. package/dist/types/components/core/trackLayoutBuilder.d.ts +1 -0
  7. package/dist/types/components/core/types.d.ts +2 -1
  8. package/dist/types/components/core/useWaveformChartController.d.ts +3 -0
  9. package/dist/types/components/core/useWaveformPresentation.d.ts +2 -0
  10. package/dist/types/components/core/waveformChartTypes.d.ts +3 -1
  11. package/dist/types/components/core/yAxisTitleLayout.d.ts +4 -0
  12. package/dist/types/components/interaction/integerZoom.d.ts +12 -0
  13. package/dist/types/components/interaction/viewportDomain.d.ts +1 -0
  14. package/dist/types/components/interaction/zoomConstraints.d.ts +2 -1
  15. package/dist/types/components/rendering/WaveformTrack.vue.d.ts +2 -6
  16. package/dist/types/components/rendering/WaveformTrackBackdrop.vue.d.ts +0 -4
  17. package/dist/types/components/rendering/WaveformZeroLines.vue.d.ts +12 -0
  18. package/dist/types/demo/types.d.ts +3 -0
  19. package/dist/types/index.d.ts +1 -0
  20. package/dist/types/types/chart.d.ts +4 -0
  21. package/package.json +1 -1
  22. package/src/App.vue +6 -0
  23. package/src/components/WaveformChart.vue +1 -0
  24. package/src/components/core/constants.ts +2 -0
  25. package/src/components/core/trackLayoutBuilder.ts +26 -56
  26. package/src/components/core/types.ts +2 -0
  27. package/src/components/core/useWaveformChartLifecycle.ts +1 -1
  28. package/src/components/core/useWaveformLayout.ts +32 -16
  29. package/src/components/core/useWaveformPresentation.ts +10 -0
  30. package/src/components/core/waveformChartTypes.ts +3 -0
  31. package/src/components/core/yAxisTitleLayout.ts +78 -0
  32. package/src/components/interaction/integerZoom.ts +64 -0
  33. package/src/components/interaction/useWaveformViewport.ts +8 -12
  34. package/src/components/interaction/useWaveformZoom.ts +6 -4
  35. package/src/components/interaction/viewportDomain.ts +12 -0
  36. package/src/components/interaction/zoomConstraints.ts +7 -5
  37. package/src/components/rendering/WaveformTrack.vue +11 -4
  38. package/src/components/rendering/WaveformTrackAxes.vue +1 -1
  39. package/src/components/rendering/WaveformTrackBackdrop.vue +2 -34
  40. package/src/components/rendering/WaveformZeroLines.vue +53 -0
  41. package/src/demo/DemoChartHost.vue +1 -0
  42. package/src/demo/DemoGridFrameControls.vue +12 -0
  43. package/src/demo/DemoViewControls.vue +4 -0
  44. package/src/demo/types.ts +3 -0
  45. package/src/index.ts +1 -0
  46. package/src/styles.css +3 -3
  47. package/src/types/chart.ts +4 -0
@@ -53,18 +53,19 @@ declare const __VLS_export: import("vue").DefineComponent<WaveformChartProps, {
53
53
  }>, {
54
54
  legend: import("..").WaveformLegendOptions;
55
55
  annotations: import("..").WaveformAnnotation[];
56
+ grid: import("..").WaveformGridOptions;
56
57
  displayMode: import("..").WaveformDisplayMode;
57
58
  overlayMode: import("..").WaveformOverlayMode;
59
+ xDomainStrategy: import("..").WaveformXDomainStrategy;
58
60
  yLabel: string;
59
61
  lineColor: string;
60
62
  showTooltip: boolean;
61
63
  zoomable: boolean;
64
+ integerZoom: boolean;
62
65
  pannable: boolean;
63
66
  minVisiblePoints: number;
64
- xDomainStrategy: import("..").WaveformXDomainStrategy;
65
67
  timeUnit: "s" | "ms";
66
68
  annotationsVisible: boolean;
67
- grid: import("..").WaveformGridOptions;
68
69
  rendering: import("..").WaveformRenderingOptions;
69
70
  plotMargin: import("..").WaveformPlotMargin;
70
71
  defaultHiddenSeriesIds: string[];
@@ -121,6 +121,8 @@ export declare const ZERO_LINE_DEFAULTS: {
121
121
  WIDTH: number;
122
122
  /** 虚线样式 */
123
123
  DASH: string;
124
+ OPACITY: number;
125
+ BOUNDARY_THRESHOLD: number;
124
126
  };
125
127
  /**
126
128
  * 图例默认配置
@@ -17,6 +17,7 @@ export interface BuildTrackLayoutsOptions {
17
17
  initialXDomain?: [number, number];
18
18
  initialXDomains?: Record<string, [number, number]>;
19
19
  xDomainStrategy?: WaveformXDomainStrategy;
20
+ integerZoom?: boolean;
20
21
  fixedYDomain?: [number, number];
21
22
  fixedYDomains?: Record<string, [number, number]>;
22
23
  yDomains?: Record<string, [number, number]>;
@@ -1,5 +1,5 @@
1
1
  import type { ScaleLinear } from 'd3';
2
- import type { ResolvedWaveformErrorBarOptions, WaveformLineType, WaveformLineStyle, WaveformPoint, WaveformPointType } from '../../types';
2
+ import type { ResolvedWaveformErrorBarOptions, WaveformLineType, WaveformLineStyle, WaveformPoint, WaveformPointType, WaveformXAxisLabelFormatter } from '../../types';
3
3
  import type { WaveformPointSource } from '../../core/waveformPointSource';
4
4
  import type { NormalizedWaveformGridLineOptions } from './grid';
5
5
  /**
@@ -104,6 +104,7 @@ export interface TrackLayout {
104
104
  yMinorTicks: number[];
105
105
  yAxisTickValues: number[];
106
106
  xAxisTickValues: number[];
107
+ xAxisLabelFormatter?: WaveformXAxisLabelFormatter;
107
108
  endpointLabels: {
108
109
  start: string;
109
110
  end: string;
@@ -298,6 +298,8 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
298
298
  color: string;
299
299
  width: number;
300
300
  dash: string;
301
+ opacity: number;
302
+ boundaryThreshold: number;
301
303
  };
302
304
  legendBackgroundColor: string;
303
305
  legendInteractive: boolean;
@@ -332,6 +334,7 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
332
334
  lineColor: string;
333
335
  showTooltip: boolean;
334
336
  zoomable: boolean;
337
+ integerZoom: boolean;
335
338
  pannable: boolean;
336
339
  minZoomSpan?: number | undefined;
337
340
  minVisiblePoints: number;
@@ -26,6 +26,8 @@ export declare function useWaveformPresentation(context: PresentationContext): {
26
26
  color: string;
27
27
  width: number;
28
28
  dash: string;
29
+ opacity: number;
30
+ boundaryThreshold: number;
29
31
  }>;
30
32
  legendBackgroundColor: import("vue").ComputedRef<string>;
31
33
  legendInteractive: import("vue").ComputedRef<boolean>;
@@ -11,6 +11,8 @@ export interface WaveformChartProps {
11
11
  lineColor?: string;
12
12
  showTooltip?: boolean;
13
13
  zoomable?: boolean;
14
+ /** Snap viewport endpoints outwards to integer X-axis display units. */
15
+ integerZoom?: boolean;
14
16
  pannable?: boolean;
15
17
  minZoomSpan?: number;
16
18
  minVisiblePoints?: number;
@@ -40,7 +42,7 @@ export interface WaveformChartProps {
40
42
  presentationMode?: boolean;
41
43
  zeroLine?: WaveformZeroLineOptions;
42
44
  }
43
- type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'pannable' | 'minVisiblePoints' | 'xDomainStrategy' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'plotMargin' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
45
+ type DefaultedProp = 'displayMode' | 'overlayMode' | 'yLabel' | 'lineColor' | 'showTooltip' | 'zoomable' | 'integerZoom' | 'pannable' | 'minVisiblePoints' | 'xDomainStrategy' | 'timeUnit' | 'annotations' | 'annotationsVisible' | 'grid' | 'rendering' | 'plotMargin' | 'legend' | 'defaultHiddenSeriesIds' | 'cleanView' | 'presentationMode' | 'zeroLine';
44
46
  export type ResolvedWaveformChartProps = Readonly<WaveformChartProps & Required<Pick<WaveformChartProps, DefaultedProp>>>;
45
47
  export interface WaveformChartEmit {
46
48
  (event: 'point-hover', point: WaveformPoint | null): void;
@@ -0,0 +1,4 @@
1
+ import type { TrackLayout } from './types';
2
+ type TitleTrack = Pick<TrackLayout, 'height' | 'column' | 'yAxes'>;
3
+ export declare function alignLeftYAxisTitles(layouts: TitleTrack[], yLabel?: string): void;
4
+ export {};
@@ -0,0 +1,12 @@
1
+ export interface IntegerZoomOptions {
2
+ integerZoom?: boolean;
3
+ timeUnit?: 's' | 'ms';
4
+ }
5
+ export declare function integerZoomTicks(domain: [number, number], count: number, options: IntegerZoomOptions): number[];
6
+ export declare function formatIntegerZoomLabel(value: number): string;
7
+ /** Align outwards in display units; all returned coordinates remain in seconds. */
8
+ export declare function alignIntegerZoomDomain(domain: [number, number], options: IntegerZoomOptions): [number, number];
9
+ /** Recover aligned endpoints after a D3 transform, tolerating floating-point round-off. */
10
+ export declare function normalizeIntegerZoomDomain(domain: [number, number], options: IntegerZoomOptions): [number, number];
11
+ /** Translation snaps to the nearest unit so panning preserves the visible span. */
12
+ export declare function alignIntegerPanDomain(domain: [number, number], options: IntegerZoomOptions): [number, number];
@@ -17,5 +17,6 @@ interface ViewportDomainContext {
17
17
  editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>;
18
18
  configureZoom: () => void;
19
19
  }
20
+ export declare function clampViewportDomain(domain: [number, number], boundary: [number, number]): [number, number];
20
21
  export declare function createViewportDomainSetter(context: ViewportDomainContext): (domain: [number, number], trackIndex?: number) => void;
21
22
  export {};
@@ -1,12 +1,13 @@
1
1
  import { type ZoomTransform } from 'd3';
2
2
  import type { WaveformPoint } from '../../types';
3
3
  import { type WaveformPointSource } from '../../core/waveformPointSource';
4
+ import { type IntegerZoomOptions } from './integerZoom';
4
5
  interface PointSeriesSource {
5
6
  points: WaveformPoint[];
6
7
  source?: WaveformPointSource;
7
8
  }
8
9
  export type ZoomSeriesGroup = readonly PointSeriesSource[];
9
- interface ZoomConstraintOptions {
10
+ interface ZoomConstraintOptions extends IntegerZoomOptions {
10
11
  minZoomSpan?: number;
11
12
  minVisiblePoints?: number;
12
13
  maxZoomScale?: number | null;
@@ -15,9 +15,7 @@ interface Props {
15
15
  timeUnit: 's' | 'ms';
16
16
  yLabel?: string;
17
17
  cleanView?: boolean;
18
- zeroLine?: Required<Pick<WaveformZeroLineOptions, 'color' | 'width' | 'dash'>> & {
19
- visible: boolean;
20
- };
18
+ zeroLine?: Required<WaveformZeroLineOptions>;
21
19
  }
22
20
  declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {} & {
23
21
  contextmenu: (mouseEvent: MouseEvent) => any;
@@ -38,9 +36,7 @@ declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {},
38
36
  }>, {
39
37
  interactive: boolean;
40
38
  cleanView: boolean;
41
- zeroLine: Required<Pick<WaveformZeroLineOptions, "color" | "width" | "dash">> & {
42
- visible: boolean;
43
- };
39
+ zeroLine: Required<WaveformZeroLineOptions>;
44
40
  interactionMode: WaveformInteractionMode;
45
41
  }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
46
42
  declare const _default: typeof __VLS_export;
@@ -1,4 +1,3 @@
1
- import type { WaveformZeroLineOptions } from '../../types';
2
1
  import type { TrackLayout } from '../core/types';
3
2
  interface Props {
4
3
  track: TrackLayout;
@@ -9,9 +8,6 @@ interface Props {
9
8
  frameStyle: {
10
9
  backgroundColor: string;
11
10
  };
12
- zeroLine: Required<Pick<WaveformZeroLineOptions, 'color' | 'width' | 'dash'>> & {
13
- visible: boolean;
14
- };
15
11
  }
16
12
  declare const __VLS_export: import("vue").DefineComponent<Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
17
13
  declare const _default: typeof __VLS_export;
@@ -0,0 +1,12 @@
1
+ import type { WaveformZeroLineOptions } from '../../types';
2
+ import type { TrackLayout } from '../core/types';
3
+ type __VLS_Props = {
4
+ track: TrackLayout;
5
+ clipPathId: string;
6
+ innerWidth: number;
7
+ cleanView: boolean;
8
+ zeroLine: Required<WaveformZeroLineOptions>;
9
+ };
10
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
11
+ declare const _default: typeof __VLS_export;
12
+ export default _default;
@@ -9,6 +9,7 @@ export interface DemoControlPanelModel {
9
9
  displayMode: WaveformDisplayMode;
10
10
  overlayMode: WaveformOverlayMode;
11
11
  showTooltip: boolean;
12
+ integerZoom: boolean;
12
13
  plotMarginTop: number;
13
14
  plotMarginBottom: number;
14
15
  cleanView: boolean;
@@ -20,6 +21,7 @@ export interface DemoControlPanelModel {
20
21
  zeroLineVisible: boolean;
21
22
  zeroLineColor: string;
22
23
  zeroLineWidth: number;
24
+ zeroLineOpacity: number;
23
25
  zeroLineDash: string;
24
26
  zeroLineDashOptions: Array<SelectOption<string>>;
25
27
  rowCount: number;
@@ -82,6 +84,7 @@ export interface DemoChartModel {
82
84
  cleanView: boolean;
83
85
  presentationMode: boolean;
84
86
  showTooltip: boolean;
87
+ integerZoom: boolean;
85
88
  plotMargin: WaveformPlotMargin;
86
89
  zeroLine: WaveformZeroLineOptions;
87
90
  frameWatermarkVisible: boolean;
@@ -3,6 +3,7 @@
3
3
  * @packageDocumentation
4
4
  */
5
5
  export { default as WaveformChart } from './components/WaveformChart.vue';
6
+ export type { WaveformChartProps } from './components/core/waveformChartTypes';
6
7
  export type { WaveformPoint, WaveformDisplayMode, WaveformOverlayMode, WaveformInteractionMode, WaveformXDomainStrategy, WaveformZoomIntentPayload, WaveformZoomEndPayload, WaveformZoomResetPayload, WaveformAnnotationStyle, WaveformAnnotation, WaveformSamplingMode, WaveformSamplingStrategy, WaveformSamplingOptions, WaveformSamplingBackend, WaveformSamplingDiagnostics, WaveformSamplingError, WaveformRenderingOptions, WaveformPlotMargin, WaveformTitleTextStyle, WaveformTitleOptions, WaveformLegendPosition, WaveformLegendOrientation, WaveformLegendOptions, WaveformFrameStyle, WaveformXAxisLabelKind, WaveformXAxisLabelFormatterContext, WaveformXAxisLabelFormatter, WaveformAxesOptions, WaveformZeroLineOptions, SingleWaveformData, WaveformLineType, WaveformLineStyle, WaveformPointType, WaveformErrorBarOptions, ResolvedWaveformErrorBarOptions, WaveformTypedValues, TypedSampleData, TypedPointData, WaveformSeries, WaveformData, NormalizedWaveformSeries, } from './types';
7
8
  export type { WaveformGridOptions, WaveformGridLineOptions, WaveformGridTrackLines, } from './components/core/grid';
8
9
  export { normalizeWaveformData, normalizeWaveformSeries } from './core';
@@ -233,4 +233,8 @@ export interface WaveformZeroLineOptions {
233
233
  width?: number;
234
234
  /** SVG stroke-dasharray. Defaults to '6 4'; use '' for a solid line. */
235
235
  dash?: string;
236
+ /** Stroke opacity from 0 to 1. Defaults to 0.5; multiplies the color's alpha. */
237
+ opacity?: number;
238
+ /** Hide within this fraction of a Y-axis tick interval from either boundary. Defaults to 0.02. */
239
+ boundaryThreshold?: number;
236
240
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.65",
3
+ "version": "0.1.66",
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
@@ -48,11 +48,13 @@ const annotationsVisible = ref(true)
48
48
  const cleanView = ref(false)
49
49
  const presentationMode = ref(false)
50
50
  const showTooltip = ref(true)
51
+ const integerZoom = ref(true)
51
52
  const plotMarginTop = ref(18)
52
53
  const plotMarginBottom = ref(52)
53
54
  const zeroLineVisible = ref(true)
54
55
  const zeroLineColor = ref('#ff0000')
55
56
  const zeroLineWidth = ref(1)
57
+ const zeroLineOpacity = ref(0.5)
56
58
  const zeroLineDash = ref('6 4')
57
59
  const interactionMode = ref<WaveformInteractionMode>('zoom')
58
60
  const legendPosition = ref<WaveformLegendPosition>('top-right')
@@ -127,6 +129,7 @@ const zeroLine = computed<WaveformZeroLineOptions>(() => ({
127
129
  visible: zeroLineVisible.value,
128
130
  color: zeroLineColor.value,
129
131
  width: zeroLineWidth.value,
132
+ opacity: zeroLineOpacity.value,
130
133
  dash: zeroLineDash.value,
131
134
  }))
132
135
  const plotMargin = computed<WaveformPlotMargin>(() => ({
@@ -298,6 +301,7 @@ const controlPanelModel = reactive({
298
301
  displayMode,
299
302
  overlayMode,
300
303
  showTooltip,
304
+ integerZoom,
301
305
  plotMarginTop,
302
306
  plotMarginBottom,
303
307
  cleanView,
@@ -309,6 +313,7 @@ const controlPanelModel = reactive({
309
313
  zeroLineVisible,
310
314
  zeroLineColor,
311
315
  zeroLineWidth,
316
+ zeroLineOpacity,
312
317
  zeroLineDash,
313
318
  zeroLineDashOptions,
314
319
  rowCount,
@@ -365,6 +370,7 @@ const chartModel = reactive({
365
370
  cleanView,
366
371
  presentationMode,
367
372
  showTooltip,
373
+ integerZoom,
368
374
  plotMargin,
369
375
  zeroLine,
370
376
  frameWatermarkVisible,
@@ -14,6 +14,7 @@ const props = withDefaults(defineProps<WaveformChartProps>(), {
14
14
  lineColor: '#0960bd',
15
15
  showTooltip: true,
16
16
  zoomable: true,
17
+ integerZoom: false,
17
18
  pannable: false,
18
19
  minVisiblePoints: 0,
19
20
  xDomainStrategy: () => ({ type: 'integer-ms' }),
@@ -182,6 +182,8 @@ export const ZERO_LINE_DEFAULTS = {
182
182
  WIDTH: 1,
183
183
  /** 虚线样式 */
184
184
  DASH: '6 4',
185
+ OPACITY: 0.5,
186
+ BOUNDARY_THRESHOLD: 0.02,
185
187
  }
186
188
 
187
189
  /**
@@ -27,7 +27,6 @@ import {
27
27
  } from './grid'
28
28
  import {
29
29
  axisTextMetrics,
30
- formatYAxisTickLabel,
31
30
  resolveRenderedYAxisSeriesGroups,
32
31
  resolveYAxisTicks,
33
32
  resolveYAxisTickCount,
@@ -35,9 +34,15 @@ import {
35
34
  } from './layout'
36
35
  import type { DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
37
36
  import { applyXDomainStrategy } from './xDomain'
37
+ import { alignLeftYAxisTitles } from './yAxisTitleLayout'
38
+ import {
39
+ alignIntegerZoomDomain,
40
+ normalizeIntegerZoomDomain,
41
+ integerZoomTicks,
42
+ formatIntegerZoomLabel,
43
+ } from '../interaction/integerZoom'
38
44
  import {
39
45
  Y_AXIS_LABEL_BAND_WIDTH,
40
- Y_AXIS_CHARACTER_WIDTH,
41
46
  Y_AXIS_LABEL_GAP,
42
47
  Y_AXIS_OUTER_PADDING,
43
48
  Y_AXIS_RIGHT_LABEL_OFFSET,
@@ -58,6 +63,7 @@ export interface BuildTrackLayoutsOptions {
58
63
  initialXDomain?: [number, number]
59
64
  initialXDomains?: Record<string, [number, number]>
60
65
  xDomainStrategy?: WaveformXDomainStrategy
66
+ integerZoom?: boolean
61
67
  fixedYDomain?: [number, number]
62
68
  fixedYDomains?: Record<string, [number, number]>
63
69
  yDomains?: Record<string, [number, number]>
@@ -85,9 +91,12 @@ function resolveIndependentXDomain(
85
91
  options.initialXDomains?.[track.id] ??
86
92
  options.initialXDomains?.[seriesId] ??
87
93
  options.initialXDomain
88
- return explicitDomain
89
- ? applyXDomainStrategy(explicitDomain, strategy, true)
90
- : applyXDomainStrategy(track.xDomain, strategy)
94
+ return alignIntegerZoomDomain(
95
+ explicitDomain
96
+ ? applyXDomainStrategy(explicitDomain, strategy, true)
97
+ : applyXDomainStrategy(track.xDomain, strategy),
98
+ options,
99
+ )
91
100
  }
92
101
 
93
102
  export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayout[] {
@@ -119,6 +128,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
119
128
  ? (options.independentTransforms[index] ?? zoomIdentity)
120
129
  : zoomIdentity
121
130
  const xScale = transform.rescaleX(baseXScale)
131
+ xScale.domain(normalizeIntegerZoomDomain(xScale.domain() as [number, number], options))
122
132
  const yAxisGroups = resolveRenderedYAxisSeriesGroups(
123
133
  displayTrack,
124
134
  options.overlayMode,
@@ -181,25 +191,19 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
181
191
  const fallbackYScale = scaleLinear(displayTrack.yDomain, [cell.plotHeight, 0])
182
192
  if (options.yAxisNice !== false) fallbackYScale.nice()
183
193
  const yScale = yAxes[0]?.scale ?? fallbackYScale
184
- const xMajorTicks = xScale.ticks(Math.max(2, Math.floor(cell.width / 100)))
194
+ const xMajorTicks = integerZoomTicks(
195
+ xScale.domain() as [number, number],
196
+ Math.max(2, Math.floor(cell.width / 100)),
197
+ options,
198
+ )
199
+ const xAxisLabelFormatter =
200
+ options.xAxisLabelFormatter ?? (options.integerZoom ? formatIntegerZoomLabel : undefined)
185
201
  const yMajorTicks = yAxes[0]?.majorTicks ?? []
186
202
  const yAxisTickValues = yAxes[0]?.tickValues ?? []
187
203
  const domain = xScale.domain() as [number, number]
188
204
  const endpointLabels = {
189
- start: formatXAxisLabel(
190
- domain[0],
191
- domain,
192
- options.timeUnit,
193
- 'start',
194
- options.xAxisLabelFormatter,
195
- ),
196
- end: formatXAxisLabel(
197
- domain[1],
198
- domain,
199
- options.timeUnit,
200
- 'end',
201
- options.xAxisLabelFormatter,
202
- ),
205
+ start: formatXAxisLabel(domain[0], domain, options.timeUnit, 'start', xAxisLabelFormatter),
206
+ end: formatXAxisLabel(domain[1], domain, options.timeUnit, 'end', xAxisLabelFormatter),
203
207
  }
204
208
  const leftClearance = endpointLabels.start.length * 7 + 10
205
209
  const rightClearance = endpointLabels.end.length * 7 + 10
@@ -262,6 +266,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
262
266
  yScale,
263
267
  yAxes,
264
268
  xMajorTicks,
269
+ xAxisLabelFormatter,
265
270
  xMinorTicks: buildMinorTicks(xMajorTicks, 3, domain),
266
271
  yMajorTicks,
267
272
  yMinorTicks: yAxes[0]?.minorTicks ?? [],
@@ -283,41 +288,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
283
288
  }
284
289
  })
285
290
 
286
- // Align names using the widest nearby tick text in each column and left-axis slot.
287
- const titleDistances = new Map<string, number>()
288
- for (const track of layouts) {
289
- let leftIndex = 0
290
- for (const axis of track.yAxes) {
291
- if (axis.side !== 'left') continue
292
- const key = `${track.column}:${leftIndex++}`
293
- const series = axis.seriesList[0]
294
- const title = series?.name.trim() || options.yLabel || ''
295
- const halfTitleHeight = title.length * 8 + 6
296
- const nearbyTicks = axis.tickValues.filter(
297
- (value) => Math.abs(axis.scale(value) - track.height / 2) <= halfTitleHeight,
298
- )
299
- const tickWidth = Math.max(
300
- Y_AXIS_CHARACTER_WIDTH,
301
- ...nearbyTicks.map(
302
- (value) =>
303
- formatYAxisTickLabel(
304
- value,
305
- axis.scale.domain() as [number, number],
306
- axis.tickValues,
307
- series?.unit,
308
- ).length * Y_AXIS_CHARACTER_WIDTH,
309
- ),
310
- )
311
- const distance = tickWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_BAND_WIDTH / 2
312
- titleDistances.set(key, Math.max(titleDistances.get(key) ?? 0, distance))
313
- }
314
- }
315
- for (const track of layouts) {
316
- let leftIndex = 0
317
- for (const axis of track.yAxes) {
318
- if (axis.side !== 'left') continue
319
- axis.labelX = axis.x - titleDistances.get(`${track.column}:${leftIndex++}`)!
320
- }
321
- }
291
+ alignLeftYAxisTitles(layouts, options.yLabel)
322
292
  return layouts
323
293
  }
@@ -5,6 +5,7 @@ import type {
5
5
  WaveformLineStyle,
6
6
  WaveformPoint,
7
7
  WaveformPointType,
8
+ WaveformXAxisLabelFormatter,
8
9
  } from '../../types'
9
10
  import type { WaveformPointSource } from '../../core/waveformPointSource'
10
11
  import type { NormalizedWaveformGridLineOptions } from './grid'
@@ -114,6 +115,7 @@ export interface TrackLayout {
114
115
  yMinorTicks: number[]
115
116
  yAxisTickValues: number[]
116
117
  xAxisTickValues: number[]
118
+ xAxisLabelFormatter?: WaveformXAxisLabelFormatter
117
119
  endpointLabels: { start: string; end: string }
118
120
  path: string | null
119
121
  seriesPaths: TrackSeriesPath[]
@@ -239,7 +239,7 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
239
239
  )
240
240
 
241
241
  watch(
242
- () => props.displayMode,
242
+ () => [props.displayMode, props.integerZoom, props.integerZoom ? props.timeUnit : undefined],
243
243
  () => {
244
244
  const previousPage = currentPage.value
245
245
  currentPage.value = 1
@@ -21,6 +21,7 @@ import { createFrameNumberResolver, resolvePageableTracks } from './trackPaginat
21
21
  import type { PreparedWaveformSeries } from './useWaveformData'
22
22
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
23
23
  import { applyXDomainStrategy } from './xDomain'
24
+ import { alignIntegerZoomDomain, normalizeIntegerZoomDomain } from '../interaction/integerZoom'
24
25
  import { resolveYAxisLayoutMetrics, resolveViewportYDomains } from './yAxisLayoutMetrics'
25
26
  import type { useWaveformAnnotationInteraction } from '../annotation'
26
27
  interface LayoutContext {
@@ -220,13 +221,19 @@ export function useWaveformLayout(context: LayoutContext) {
220
221
  Number.isFinite(domain[1]) &&
221
222
  domain[0] !== domain[1]
222
223
  ) {
223
- return applyXDomainStrategy(
224
- domain[0] < domain[1] ? domain : [domain[1], domain[0]],
225
- props.xDomainStrategy,
226
- true,
224
+ return alignIntegerZoomDomain(
225
+ applyXDomainStrategy(
226
+ domain[0] < domain[1] ? domain : [domain[1], domain[0]],
227
+ props.xDomainStrategy,
228
+ true,
229
+ ),
230
+ props,
227
231
  )
228
232
  }
229
- return applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy)
233
+ return alignIntegerZoomDomain(
234
+ applyXDomainStrategy(sharedXDomain.value, props.xDomainStrategy),
235
+ props,
236
+ )
230
237
  })
231
238
  const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
232
239
  const configuredDomain =
@@ -239,24 +246,32 @@ export function useWaveformLayout(context: LayoutContext) {
239
246
  Number.isFinite(configuredDomain[1]) &&
240
247
  configuredDomain[0] !== configuredDomain[1]
241
248
  ) {
242
- return applyXDomainStrategy(
243
- configuredDomain[0] < configuredDomain[1]
244
- ? configuredDomain
245
- : [configuredDomain[1], configuredDomain[0]],
246
- props.xDomainStrategy,
247
- true,
249
+ return alignIntegerZoomDomain(
250
+ applyXDomainStrategy(
251
+ configuredDomain[0] < configuredDomain[1]
252
+ ? configuredDomain
253
+ : [configuredDomain[1], configuredDomain[0]],
254
+ props.xDomainStrategy,
255
+ true,
256
+ ),
257
+ props,
248
258
  )
249
259
  }
250
- return applyXDomainStrategy(
251
- paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
252
- props.xDomainStrategy,
260
+ return alignIntegerZoomDomain(
261
+ applyXDomainStrategy(
262
+ paddedDomain(track.seriesList.flatMap((series) => series.xDomain)),
263
+ props.xDomainStrategy,
264
+ ),
265
+ props,
253
266
  )
254
267
  }
255
- const sharedZoomDomain = computed(
256
- () =>
268
+ const sharedZoomDomain = computed(() =>
269
+ normalizeIntegerZoomDomain(
257
270
  sharedTransform.value
258
271
  .rescaleX(scaleLinear(initialXDomain.value, [0, innerWidth.value]))
259
272
  .domain() as [number, number],
273
+ props,
274
+ ),
260
275
  )
261
276
  const gridCells = computed(() => {
262
277
  const cells = resolveGridCellGeometry(
@@ -280,6 +295,7 @@ export function useWaveformLayout(context: LayoutContext) {
280
295
  initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
281
296
  initialXDomains: props.initialXDomains,
282
297
  xDomainStrategy: props.xDomainStrategy,
298
+ integerZoom: props.integerZoom,
283
299
  fixedYDomain: props.yDomain,
284
300
  fixedYDomains: props.yDomains,
285
301
  yDomains: viewportYDomains.value,
@@ -53,6 +53,8 @@ export function useWaveformPresentation(context: PresentationContext) {
53
53
  const isPresentationMode = computed(() => props.presentationMode === true)
54
54
  const resolvedZeroLine = computed(() => {
55
55
  const width = props.zeroLine.width
56
+ const opacity = props.zeroLine.opacity
57
+ const threshold = props.zeroLine.boundaryThreshold
56
58
  return {
57
59
  visible: props.zeroLine.visible !== false,
58
60
  color: props.zeroLine.color || ZERO_LINE_DEFAULTS.COLOR,
@@ -61,6 +63,14 @@ export function useWaveformPresentation(context: PresentationContext) {
61
63
  ? width
62
64
  : ZERO_LINE_DEFAULTS.WIDTH,
63
65
  dash: props.zeroLine.dash ?? ZERO_LINE_DEFAULTS.DASH,
66
+ opacity:
67
+ typeof opacity === 'number' && Number.isFinite(opacity) && opacity >= 0 && opacity <= 1
68
+ ? opacity
69
+ : ZERO_LINE_DEFAULTS.OPACITY,
70
+ boundaryThreshold:
71
+ typeof threshold === 'number' && Number.isFinite(threshold) && threshold >= 0
72
+ ? threshold
73
+ : ZERO_LINE_DEFAULTS.BOUNDARY_THRESHOLD,
64
74
  }
65
75
  })
66
76
  const legendBackgroundColor = computed(
@@ -33,6 +33,8 @@ export interface WaveformChartProps {
33
33
  lineColor?: string
34
34
  showTooltip?: boolean
35
35
  zoomable?: boolean
36
+ /** Snap viewport endpoints outwards to integer X-axis display units. */
37
+ integerZoom?: boolean
36
38
  pannable?: boolean
37
39
  minZoomSpan?: number
38
40
  minVisiblePoints?: number
@@ -70,6 +72,7 @@ type DefaultedProp =
70
72
  | 'lineColor'
71
73
  | 'showTooltip'
72
74
  | 'zoomable'
75
+ | 'integerZoom'
73
76
  | 'pannable'
74
77
  | 'minVisiblePoints'
75
78
  | 'xDomainStrategy'