waveform-analysis 0.1.24 → 0.1.25

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 (37) hide show
  1. package/README.md +40 -1
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.js +534 -481
  4. package/dist/types/App.vue.d.ts +4 -0
  5. package/dist/types/DemoRouterApp.vue.d.ts +3 -0
  6. package/dist/types/components/core/layout.d.ts +6 -3
  7. package/dist/types/components/core/trackLayoutBuilder.d.ts +2 -0
  8. package/dist/types/components/core/useWaveformChartController.d.ts +2 -0
  9. package/dist/types/components/core/waveformChartTypes.d.ts +2 -0
  10. package/dist/types/components/core/yDomain.d.ts +7 -0
  11. package/dist/types/demo/DemoChartHost.vue.d.ts +14 -0
  12. package/dist/types/demo/DemoControlPanel.vue.d.ts +11 -0
  13. package/dist/types/demo/DemoGridFrameControls.vue.d.ts +11 -0
  14. package/dist/types/demo/DemoTitleLegendControls.vue.d.ts +11 -0
  15. package/dist/types/demo/DemoViewControls.vue.d.ts +11 -0
  16. package/dist/types/demo/types.d.ts +84 -0
  17. package/dist/types/router.d.ts +3 -0
  18. package/dist/types/views/FixedYDomainDemo.vue.d.ts +3 -0
  19. package/package.json +2 -1
  20. package/src/DemoRouterApp.vue +14 -0
  21. package/src/components/core/fixedYDomainLayout.test.ts +116 -0
  22. package/src/components/core/layout.ts +43 -10
  23. package/src/components/core/trackLayoutBuilder.ts +17 -7
  24. package/src/components/core/useWaveformChartController.ts +10 -0
  25. package/src/components/core/useWaveformLayout.ts +20 -4
  26. package/src/components/core/waveformChartTypes.ts +2 -0
  27. package/src/components/core/yDomain.test.ts +56 -0
  28. package/src/components/core/yDomain.ts +50 -0
  29. package/src/components/interaction/useWaveformViewport.ts +6 -1
  30. package/src/components/waveformChartCases/layoutAndDomains.test.ts +25 -0
  31. package/src/components/waveformChartCases/viewportBoxAndPan.test.ts +50 -0
  32. package/src/demoRoutes.css +197 -0
  33. package/src/main.ts +4 -2
  34. package/src/router.ts +21 -0
  35. package/src/styles.css +1 -2
  36. package/src/views/FixedYDomainDemo.test.ts +44 -0
  37. package/src/views/FixedYDomainDemo.vue +149 -0
@@ -0,0 +1,4 @@
1
+ import 'vue3-colorpicker/style.css';
2
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
3
+ declare const _default: typeof __VLS_export;
4
+ export default _default;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -1,20 +1,23 @@
1
1
  import type { WaveformOverlayMode } from '../../types';
2
2
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types';
3
+ import { type WaveformYDomain } from './yDomain';
3
4
  export { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants';
4
- interface YAxisSeriesGroup {
5
+ export interface YAxisSeriesGroup {
5
6
  index: number;
6
7
  side: 'left' | 'right';
7
8
  seriesList: DisplaySeries[];
8
9
  domain: [number, number];
10
+ fixed: boolean;
9
11
  }
10
12
  export declare function buildYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode): YAxisSeriesGroup[];
11
- export declare function axisTextMetrics(domain: [number, number]): {
13
+ export declare function resolveYAxisSeriesGroups(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): YAxisSeriesGroup[];
14
+ export declare function axisTextMetrics(domain: [number, number], nice?: boolean): {
12
15
  exponentLabel: string | null;
13
16
  exponentWidth: number;
14
17
  tickTextWidth: number;
15
18
  };
16
19
  export declare function measureYAxisGroupClearance(group: YAxisSeriesGroup): number;
17
- export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode): {
20
+ export declare function measureTrackYAxisClearance(track: DisplayTrack, overlayMode: WaveformOverlayMode, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): {
18
21
  left: number;
19
22
  right: number;
20
23
  };
@@ -15,6 +15,8 @@ export interface BuildTrackLayoutsOptions {
15
15
  sharedZoomDomain: [number, number];
16
16
  initialXDomain?: [number, number];
17
17
  initialXDomains?: Record<string, [number, number]>;
18
+ fixedYDomain?: [number, number];
19
+ fixedYDomains?: Record<string, [number, number]>;
18
20
  yDomains?: Record<string, [number, number]>;
19
21
  timeUnit: 's' | 'ms';
20
22
  rendering: ResolvedWaveformRenderingOptions;
@@ -338,6 +338,8 @@ export declare function useWaveformChartController(props: ResolvedWaveformChartP
338
338
  minZoomSpan?: number | undefined;
339
339
  minVisiblePoints: number;
340
340
  initialXDomains?: Record<string, [number, number]> | undefined;
341
+ yDomain?: [number, number] | undefined;
342
+ yDomains?: Record<string, [number, number]> | undefined;
341
343
  timeUnit: "s" | "ms";
342
344
  frameNumber?: string | number | undefined;
343
345
  frameStyle?: import("..").WaveformFrameStyle | undefined;
@@ -16,6 +16,8 @@ export interface WaveformChartProps {
16
16
  minVisiblePoints?: number;
17
17
  initialXDomain?: [number, number];
18
18
  initialXDomains?: Record<string, [number, number]>;
19
+ yDomain?: [number, number];
20
+ yDomains?: Record<string, [number, number]>;
19
21
  timeUnit?: 's' | 'ms';
20
22
  frameNumber?: string | number;
21
23
  frameStyle?: WaveformFrameStyle;
@@ -0,0 +1,7 @@
1
+ import type { DisplaySeries, DisplayTrack } from './types';
2
+ export type WaveformYDomain = [number, number];
3
+ export declare function normalizeYDomain(domain: readonly [number, number] | undefined): WaveformYDomain | undefined;
4
+ export declare function resolveTrackFixedYDomain(track: Pick<DisplayTrack, 'id'>, yDomains?: Record<string, WaveformYDomain>): WaveformYDomain | undefined;
5
+ export declare function resolveSeriesFixedYDomain(series: Pick<DisplaySeries, 'id'>, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): WaveformYDomain | undefined;
6
+ export declare function hasFixedYDomainForTrack(track: Pick<DisplayTrack, 'id' | 'visibleSeries'>, yDomain?: WaveformYDomain, yDomains?: Record<string, WaveformYDomain>): boolean;
7
+ export declare function mergeYDomains(domains: readonly WaveformYDomain[]): WaveformYDomain;
@@ -0,0 +1,14 @@
1
+ import type { DemoChartModel } from './types';
2
+ declare function resetViewport(): void;
3
+ type __VLS_ModelProps = {
4
+ 'model': DemoChartModel;
5
+ };
6
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {
7
+ resetViewport: typeof resetViewport;
8
+ }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
9
+ "update:model": (value: DemoChartModel) => any;
10
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
11
+ "onUpdate:model"?: ((value: DemoChartModel) => any) | undefined;
12
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
13
+ declare const _default: typeof __VLS_export;
14
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import type { DemoControlPanelModel } from './types';
2
+ type __VLS_ModelProps = {
3
+ 'model': DemoControlPanelModel;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:model": (value: DemoControlPanelModel) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:model"?: ((value: DemoControlPanelModel) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import type { DemoControlPanelModel } from './types';
2
+ type __VLS_ModelProps = {
3
+ 'model': DemoControlPanelModel;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:model": (value: DemoControlPanelModel) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:model"?: ((value: DemoControlPanelModel) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import type { DemoControlPanelModel } from './types';
2
+ type __VLS_ModelProps = {
3
+ 'model': DemoControlPanelModel;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:model": (value: DemoControlPanelModel) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:model"?: ((value: DemoControlPanelModel) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,11 @@
1
+ import type { DemoControlPanelModel } from './types';
2
+ type __VLS_ModelProps = {
3
+ 'model': DemoControlPanelModel;
4
+ };
5
+ declare const __VLS_export: import("vue").DefineComponent<__VLS_ModelProps, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
6
+ "update:model": (value: DemoControlPanelModel) => any;
7
+ }, string, import("vue").PublicProps, Readonly<__VLS_ModelProps> & Readonly<{
8
+ "onUpdate:model"?: ((value: DemoControlPanelModel) => any) | undefined;
9
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
10
+ declare const _default: typeof __VLS_export;
11
+ export default _default;
@@ -0,0 +1,84 @@
1
+ import type { WaveformAnnotation, WaveformAxesOptions, WaveformData, WaveformDisplayMode, WaveformFrameStyle, WaveformGridTrackLines, WaveformInteractionMode, WaveformLegendOrientation, WaveformLegendPosition, WaveformLineStyle, WaveformOverlayMode, WaveformTitleOptions, WaveformZeroLineOptions, WaveformZoomEndPayload } from '../components';
2
+ interface SelectOption<T> {
3
+ label: string;
4
+ value: T;
5
+ }
6
+ export interface DemoControlPanelModel {
7
+ controlsOpen: boolean;
8
+ displayMode: WaveformDisplayMode;
9
+ overlayMode: WaveformOverlayMode;
10
+ showTooltip: boolean;
11
+ cleanView: boolean;
12
+ presentationMode: boolean;
13
+ selectedSeriesId: string;
14
+ selectedLineStyle: WaveformLineStyle;
15
+ lineStyleOptions: Array<SelectOption<WaveformLineStyle>>;
16
+ seriesStyleOptions: Array<SelectOption<string>>;
17
+ zeroLineVisible: boolean;
18
+ zeroLineColor: string;
19
+ zeroLineWidth: number;
20
+ zeroLineDash: string;
21
+ zeroLineDashOptions: Array<SelectOption<string>>;
22
+ rowCount: number;
23
+ columnCount: number;
24
+ horizontalGridVisible: boolean;
25
+ horizontalGridColor: string;
26
+ verticalGridVisible: boolean;
27
+ verticalGridColor: string;
28
+ xAxisLineVisible: boolean;
29
+ yAxisLineVisible: boolean;
30
+ frameBorderColor: string;
31
+ frameBackgroundColor: string;
32
+ frameBorderWidth: number;
33
+ frameBorderStyle: NonNullable<WaveformFrameStyle['borderStyle']>;
34
+ frameBorderStyleOptions: Array<SelectOption<string>>;
35
+ frameWatermarkVisible: boolean;
36
+ titleVisible: boolean;
37
+ titleText: string;
38
+ titleAlign: NonNullable<WaveformTitleOptions['align']>;
39
+ titleAlignOptions: Array<SelectOption<NonNullable<WaveformTitleOptions['align']>>>;
40
+ titleFontFamily: string;
41
+ titleFontFamilyOptions: Array<SelectOption<string>>;
42
+ titleFontSize: number;
43
+ titleBold: boolean;
44
+ titleItalic: boolean;
45
+ titleUnderline: boolean;
46
+ titleRotation: number;
47
+ titleColor: string;
48
+ legendPosition: WaveformLegendPosition;
49
+ legendPositionOptions: Array<SelectOption<WaveformLegendPosition>>;
50
+ legendOrientation: WaveformLegendOrientation;
51
+ legendOrientationOptions: Array<SelectOption<WaveformLegendOrientation>>;
52
+ legendBackgroundColor: string;
53
+ closeControls: () => void;
54
+ resetTitleTextStyle: () => void;
55
+ resetWaveformViewport: () => void;
56
+ }
57
+ export interface DemoChartModel {
58
+ data: WaveformData;
59
+ minZoomSpan?: number;
60
+ initialXDomain?: [number, number];
61
+ displayMode: WaveformDisplayMode;
62
+ overlayMode: WaveformOverlayMode;
63
+ rowCount: number;
64
+ columnCount: number;
65
+ gridTrackLines: WaveformGridTrackLines;
66
+ title: WaveformTitleOptions;
67
+ legendPosition: WaveformLegendPosition;
68
+ legendOrientation: WaveformLegendOrientation;
69
+ legendBackgroundColor: string;
70
+ frameStyle: WaveformFrameStyle;
71
+ axes: WaveformAxesOptions;
72
+ cleanView: boolean;
73
+ presentationMode: boolean;
74
+ showTooltip: boolean;
75
+ zeroLine: WaveformZeroLineOptions;
76
+ frameWatermarkVisible: boolean;
77
+ annotations: WaveformAnnotation[];
78
+ annotationsVisible: boolean;
79
+ interactionMode: WaveformInteractionMode;
80
+ hiddenSeriesIds: string[];
81
+ handleZoomEnd: (payload: WaveformZoomEndPayload) => void | Promise<void>;
82
+ resetWaveformViewport: () => void;
83
+ }
84
+ export {};
@@ -0,0 +1,3 @@
1
+ import { type RouteRecordRaw } from 'vue-router';
2
+ export declare const routes: RouteRecordRaw[];
3
+ export declare const router: import("vue-router").Router;
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "waveform-analysis",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
4
4
  "main": "./dist/index.cjs",
5
5
  "module": "./dist/index.js",
6
6
  "types": "./dist/types/index.d.ts",
@@ -24,6 +24,7 @@
24
24
  "dependencies": {
25
25
  "ant-design-vue": "3.2.20",
26
26
  "d3": "^7.9.0",
27
+ "vue-router": "^4.6.4",
27
28
  "vue3-colorpicker": "^2.3.0"
28
29
  },
29
30
  "peerDependencies": {
@@ -0,0 +1,14 @@
1
+ <template>
2
+ <div class="demo-shell">
3
+ <header class="demo-shell__header">
4
+ <RouterLink class="demo-shell__brand" to="/">Waveform Analysis</RouterLink>
5
+ <nav class="demo-shell__nav" aria-label="示例导航">
6
+ <RouterLink to="/">综合示例</RouterLink>
7
+ <RouterLink to="/fixed-y-domain">固定振幅范围</RouterLink>
8
+ </nav>
9
+ </header>
10
+ <div class="demo-shell__content">
11
+ <RouterView />
12
+ </div>
13
+ </div>
14
+ </template>
@@ -0,0 +1,116 @@
1
+ import { zoomIdentity } from 'd3'
2
+ import { describe, expect, it } from 'vitest'
3
+
4
+ import { DEFAULT_WAVEFORM_RENDERING_OPTIONS } from '../../core'
5
+ import type { DisplaySeries, DisplayTrack } from './types'
6
+ import { buildTrackLayouts, resolveYAxisSeriesGroups } from './layout'
7
+
8
+ function series(id: string, minimum: number, maximum: number): DisplaySeries {
9
+ return {
10
+ id,
11
+ name: id,
12
+ color: '#1677ff',
13
+ lineType: 'linear',
14
+ lineStyle: 'solid',
15
+ pointType: 'none',
16
+ errorBar: { visible: false, width: 1.5, capWidth: 8 },
17
+ points: [
18
+ { x: 0, y: minimum },
19
+ { x: 1, y: maximum },
20
+ ],
21
+ xDomain: [0, 1],
22
+ yDomain: [minimum, maximum],
23
+ hasErrorPoints: false,
24
+ }
25
+ }
26
+
27
+ function track(seriesList: DisplaySeries[]): DisplayTrack {
28
+ return {
29
+ id: 'track',
30
+ series: seriesList,
31
+ visibleSeries: seriesList,
32
+ xDomain: [0, 1],
33
+ yDomain: [0, 100],
34
+ }
35
+ }
36
+
37
+ describe('fixed Y-domain layout', () => {
38
+ it('uses an exact global fixed domain without applying nice bounds', () => {
39
+ const sourceTrack = track([series('a', 0, 100)])
40
+ const result = buildTrackLayouts({
41
+ cells: [
42
+ {
43
+ slotIndex: 0,
44
+ row: 0,
45
+ column: 0,
46
+ left: 0,
47
+ top: 0,
48
+ width: 120,
49
+ height: 100,
50
+ plotHeight: 100,
51
+ cellHeight: 130,
52
+ xAxisBand: 30,
53
+ series: sourceTrack,
54
+ },
55
+ ],
56
+ grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
57
+ displayMode: 'independent',
58
+ overlayMode: 'single-axis',
59
+ independentTransforms: [zoomIdentity],
60
+ sharedZoomDomain: [0, 1],
61
+ fixedYDomain: [3, 97],
62
+ timeUnit: 'ms',
63
+ rendering: DEFAULT_WAVEFORM_RENDERING_OPTIONS,
64
+ hideSecondaryLabels: false,
65
+ yAxisLabelX: -50,
66
+ showCompactEmptyTracks: false,
67
+ })[0]
68
+
69
+ expect(result?.yScale.domain()).toEqual([3, 97])
70
+ expect(result?.yAxes[0]?.tickValues).toContain(3)
71
+ expect(result?.yAxes[0]?.tickValues).toContain(97)
72
+ expect(result?.seriesPaths[0]?.yScale.domain()).toEqual([3, 97])
73
+ })
74
+
75
+ it('resolves track, series, and global fixed-domain precedence', () => {
76
+ const sourceTrack = track([series('a', 0, 10), series('b', 20, 30)])
77
+
78
+ expect(
79
+ resolveYAxisSeriesGroups(sourceTrack, 'single-axis', [-5, 5], {
80
+ a: [-10, 10],
81
+ track: [300, 100],
82
+ })[0]?.domain,
83
+ ).toEqual([100, 300])
84
+
85
+ expect(
86
+ resolveYAxisSeriesGroups(sourceTrack, 'single-axis', [-5, 5], {
87
+ a: [-10, 10],
88
+ })[0]?.domain,
89
+ ).toEqual([-10, 10])
90
+ })
91
+
92
+ it('keeps per-series fixed domains on separate axes and merges axis overflow', () => {
93
+ const sourceTrack = track([
94
+ series('a', 0, 1),
95
+ series('b', 10, 11),
96
+ series('c', 20, 21),
97
+ series('d', 30, 31),
98
+ series('e', 40, 41),
99
+ ])
100
+ const groups = resolveYAxisSeriesGroups(sourceTrack, 'multi-axis', undefined, {
101
+ a: [-1, 1],
102
+ b: [-2, 2],
103
+ c: [-3, 3],
104
+ d: [-4, 4],
105
+ e: [-5, 5],
106
+ })
107
+
108
+ expect(groups.map((group) => group.domain)).toEqual([
109
+ [-1, 1],
110
+ [-2, 2],
111
+ [-3, 3],
112
+ [-5, 5],
113
+ ])
114
+ expect(groups.every((group) => group.fixed)).toBe(true)
115
+ })
116
+ })
@@ -4,6 +4,12 @@ import type { WaveformOverlayMode } from '../../types'
4
4
  import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
5
5
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
6
6
  import { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
7
+ import {
8
+ mergeYDomains,
9
+ resolveSeriesFixedYDomain,
10
+ resolveTrackFixedYDomain,
11
+ type WaveformYDomain,
12
+ } from './yDomain'
7
13
 
8
14
  // 导出常量供外部使用
9
15
  export { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
@@ -14,11 +20,12 @@ const Y_AXIS_OUTER_PADDING = 4
14
20
  const Y_AXIS_LABEL_GAP = 6
15
21
  const Y_AXIS_LABEL_BAND_WIDTH = 24
16
22
 
17
- interface YAxisSeriesGroup {
23
+ export interface YAxisSeriesGroup {
18
24
  index: number
19
25
  side: 'left' | 'right'
20
26
  seriesList: DisplaySeries[]
21
27
  domain: [number, number]
28
+ fixed: boolean
22
29
  }
23
30
 
24
31
  function resolveAxisSides(axisCount: number): Array<'left' | 'right'> {
@@ -53,6 +60,7 @@ export function buildYAxisSeriesGroups(
53
60
  side: sides[index],
54
61
  seriesList: [] as DisplaySeries[],
55
62
  domain: [0, 1] as [number, number],
63
+ fixed: false,
56
64
  }))
57
65
 
58
66
  track.visibleSeries.forEach((series, index) => {
@@ -72,12 +80,35 @@ export function buildYAxisSeriesGroups(
72
80
  return grouped
73
81
  }
74
82
 
75
- export function axisTextMetrics(domain: [number, number]): {
83
+ export function resolveYAxisSeriesGroups(
84
+ track: DisplayTrack,
85
+ overlayMode: WaveformOverlayMode,
86
+ yDomain?: WaveformYDomain,
87
+ yDomains?: Record<string, WaveformYDomain>,
88
+ ): YAxisSeriesGroup[] {
89
+ const trackDomain = resolveTrackFixedYDomain(track, yDomains)
90
+ return buildYAxisSeriesGroups(track, overlayMode).map((group) => {
91
+ if (trackDomain) return { ...group, domain: trackDomain, fixed: true }
92
+ const seriesDomains = group.seriesList.map(
93
+ (series) => resolveSeriesFixedYDomain(series, yDomain, yDomains) ?? series.yDomain,
94
+ )
95
+ const fixed = group.seriesList.some((series) =>
96
+ resolveSeriesFixedYDomain(series, yDomain, yDomains),
97
+ )
98
+ return fixed ? { ...group, domain: mergeYDomains(seriesDomains), fixed } : group
99
+ })
100
+ }
101
+
102
+ export function axisTextMetrics(
103
+ domain: [number, number],
104
+ nice = true,
105
+ ): {
76
106
  exponentLabel: string | null
77
107
  exponentWidth: number
78
108
  tickTextWidth: number
79
109
  } {
80
- const scale = scaleLinear(domain, [1, 0]).nice()
110
+ const scale = scaleLinear(domain, [1, 0])
111
+ if (nice) scale.nice()
81
112
  const [axisMin, axisMax] = scale.domain()
82
113
  const values = scale.ticks(10)
83
114
  const maximumTickCharacters = Math.max(
@@ -92,15 +123,15 @@ export function axisTextMetrics(domain: [number, number]): {
92
123
  }
93
124
  }
94
125
 
95
- function axisExponentClearance(domain: [number, number]): number {
96
- const { exponentLabel, exponentWidth } = axisTextMetrics(domain)
126
+ function axisExponentClearance(domain: [number, number], nice: boolean): number {
127
+ const { exponentLabel, exponentWidth } = axisTextMetrics(domain, nice)
97
128
  return exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
98
129
  }
99
130
 
100
131
  export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
101
132
  return (
102
- axisTextMetrics(group.domain).tickTextWidth +
103
- axisExponentClearance(group.domain) +
133
+ axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
134
+ axisExponentClearance(group.domain, !group.fixed) +
104
135
  Y_AXIS_TICK_PADDING +
105
136
  Y_AXIS_LABEL_GAP +
106
137
  Y_AXIS_LABEL_BAND_WIDTH +
@@ -110,8 +141,8 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
110
141
 
111
142
  function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
112
143
  return (
113
- axisTextMetrics(group.domain).tickTextWidth +
114
- axisExponentClearance(group.domain) +
144
+ axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
145
+ axisExponentClearance(group.domain, !group.fixed) +
115
146
  Y_AXIS_TICK_PADDING +
116
147
  Y_AXIS_OUTER_PADDING
117
148
  )
@@ -120,8 +151,10 @@ function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
120
151
  export function measureTrackYAxisClearance(
121
152
  track: DisplayTrack,
122
153
  overlayMode: WaveformOverlayMode,
154
+ yDomain?: WaveformYDomain,
155
+ yDomains?: Record<string, WaveformYDomain>,
123
156
  ): { left: number; right: number } {
124
- return buildYAxisSeriesGroups(track, overlayMode).reduce(
157
+ return resolveYAxisSeriesGroups(track, overlayMode, yDomain, yDomains).reduce(
125
158
  (clearance, group) => {
126
159
  clearance[group.side] +=
127
160
  overlayMode === 'multi-axis' || track.visibleSeries.length === 1
@@ -19,7 +19,7 @@ import {
19
19
  type GridCellGeometry,
20
20
  type NormalizedWaveformGridOptions,
21
21
  } from './grid'
22
- import { axisTextMetrics, buildYAxisSeriesGroups } from './layout'
22
+ import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
23
23
  import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
24
24
  import { Y_AXIS_EXPONENT_GAP } from './constants'
25
25
  import {
@@ -42,6 +42,8 @@ export interface BuildTrackLayoutsOptions {
42
42
  sharedZoomDomain: [number, number]
43
43
  initialXDomain?: [number, number]
44
44
  initialXDomains?: Record<string, [number, number]>
45
+ fixedYDomain?: [number, number]
46
+ fixedYDomains?: Record<string, [number, number]>
45
47
  yDomains?: Record<string, [number, number]>
46
48
  timeUnit: 's' | 'ms'
47
49
  rendering: ResolvedWaveformRenderingOptions
@@ -94,13 +96,18 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
94
96
  : zoomIdentity
95
97
  const xScale = transform.rescaleX(baseXScale)
96
98
  const configuredYDomain = options.yDomains?.[displayTrack.id]
97
- const yAxisGroups = buildYAxisSeriesGroups(displayTrack, options.overlayMode).map((group) => ({
98
- ...group,
99
- domain: configuredYDomain ?? group.domain,
100
- }))
99
+ const yAxisGroups = resolveYAxisSeriesGroups(
100
+ displayTrack,
101
+ options.overlayMode,
102
+ options.fixedYDomain,
103
+ options.fixedYDomains,
104
+ ).map((group) =>
105
+ !group.fixed && configuredYDomain ? { ...group, domain: configuredYDomain } : group,
106
+ )
101
107
  const sideOffsets = { left: 0, right: 0 }
102
108
  const yAxes: WaveformYAxisLayout[] = yAxisGroups.map((group) => {
103
- const scale = scaleLinear(group.domain, [cell.plotHeight, 0]).nice()
109
+ const scale = scaleLinear(group.domain, [cell.plotHeight, 0])
110
+ if (!group.fixed) scale.nice()
104
111
  const majorTicks = scale.ticks(Math.max(2, Math.floor(cell.plotHeight / 55)))
105
112
  const [axisStart, axisEnd] = scale.domain()
106
113
  const showAxisEnd = options.displayMode !== 'compact' || cell.row === 0
@@ -110,7 +117,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
110
117
  const tickValues = Array.from(
111
118
  new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
112
119
  )
113
- const { exponentLabel, exponentWidth, tickTextWidth } = axisTextMetrics(group.domain)
120
+ const { exponentLabel, exponentWidth, tickTextWidth } = axisTextMetrics(
121
+ group.domain,
122
+ !group.fixed,
123
+ )
114
124
  const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
115
125
  const clearance =
116
126
  tickTextWidth +
@@ -6,6 +6,7 @@ import {
6
6
  shallowReactive,
7
7
  shallowRef,
8
8
  toRefs,
9
+ watch,
9
10
  watchEffect,
10
11
  type ComponentPublicInstance,
11
12
  type Ref,
@@ -68,6 +69,15 @@ export function useWaveformChartController(
68
69
  const annotationInteraction = useWaveformAnnotationInteraction()
69
70
  const editorSeriesOptions = ref<AnnotationSeriesCandidate[]>([])
70
71
  const hoverThrottle = useAnimationFrameThrottle()
72
+
73
+ watch(
74
+ [() => props.yDomain, () => props.yDomains],
75
+ () => {
76
+ sharedYDomains.value = {}
77
+ independentYDomains.value = {}
78
+ },
79
+ { deep: true },
80
+ )
71
81
  const selection = ref<ViewportSelectionState | null>(null)
72
82
  const spacePressed = ref(false)
73
83
  const pointerInsideChart = ref(false)
@@ -26,7 +26,12 @@ import {
26
26
  resolveGridCellGeometry,
27
27
  X_AXIS_BAND,
28
28
  } from './grid'
29
- import { buildTrackLayouts, measureTrackYAxisClearance, Y_AXIS_EXPONENT_GAP } from './layout'
29
+ import {
30
+ buildTrackLayouts,
31
+ measureTrackYAxisClearance,
32
+ resolveYAxisSeriesGroups,
33
+ Y_AXIS_EXPONENT_GAP,
34
+ } from './layout'
30
35
  import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
31
36
  import type { PreparedWaveformSeries } from './useWaveformData'
32
37
  import type { ResolvedWaveformChartProps } from './waveformChartTypes'
@@ -104,8 +109,12 @@ export function useWaveformLayout(context: LayoutContext) {
104
109
  const yAxisMetrics = computed(() => {
105
110
  const axisText = chartTracks.value
106
111
  .filter((track) => track.visibleSeries.length > 0)
107
- .map((track) => {
108
- const scale = scaleLinear(track.yDomain, [1, 0]).nice()
112
+ .flatMap((track) =>
113
+ resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
114
+ )
115
+ .map((group) => {
116
+ const scale = scaleLinear(group.domain, [1, 0])
117
+ if (!group.fixed) scale.nice()
109
118
  const [axisMin, axisMax] = scale.domain()
110
119
  return {
111
120
  exponentLabel: formatScientificAxisExponent(axisMin, axisMax),
@@ -165,7 +174,12 @@ export function useWaveformLayout(context: LayoutContext) {
165
174
  const multiAxisClearance = computed(() =>
166
175
  chartTracks.value.reduce(
167
176
  (maximum, track) => {
168
- const clearance = measureTrackYAxisClearance(track, props.overlayMode)
177
+ const clearance = measureTrackYAxisClearance(
178
+ track,
179
+ props.overlayMode,
180
+ props.yDomain,
181
+ props.yDomains,
182
+ )
169
183
  return {
170
184
  left: Math.max(maximum.left, clearance.left),
171
185
  right: Math.max(maximum.right, clearance.right),
@@ -281,6 +295,8 @@ export function useWaveformLayout(context: LayoutContext) {
281
295
  sharedZoomDomain: sharedZoomDomain.value,
282
296
  initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
283
297
  initialXDomains: props.initialXDomains,
298
+ fixedYDomain: props.yDomain,
299
+ fixedYDomains: props.yDomains,
284
300
  yDomains:
285
301
  props.displayMode === 'independent'
286
302
  ? Object.fromEntries(
@@ -31,6 +31,8 @@ export interface WaveformChartProps {
31
31
  minVisiblePoints?: number
32
32
  initialXDomain?: [number, number]
33
33
  initialXDomains?: Record<string, [number, number]>
34
+ yDomain?: [number, number]
35
+ yDomains?: Record<string, [number, number]>
34
36
  timeUnit?: 's' | 'ms'
35
37
  frameNumber?: string | number
36
38
  frameStyle?: WaveformFrameStyle