From 6a6a387868721f1722aff34646fbf4783255ed21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E5=90=AF=E6=BA=90?= Date: Tue, 21 Jul 2026 20:44:08 +0800 Subject: [PATCH] feat(chart): add zero line and clean view --- README.md | 40 +++++- src/App.test.ts | 23 ++++ src/App.vue | 66 ++++++++++ src/components/WaveformChart.test.ts | 146 +++++++++++++++++++++ src/components/WaveformChart.vue | 76 ++++++++--- src/components/core/grid.ts | 5 +- src/components/data/types.ts | 1 + src/components/index.ts | 1 + src/components/rendering/WaveformTrack.vue | 68 ++++++++-- src/index.ts | 1 + src/styles.css | 3 +- src/types/chart.ts | 8 ++ src/types/index.ts | 1 + 13 files changed, 401 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index ae5349d..c4e0179 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ pnpm add waveform-analysis vue d3 ant-design-vue vue3-colorpicker 组件库支持以下运行时版本: -| 依赖 | 支持版本 | -| --- | --- | -| Vue | `>=3.2.33 <4` | +| 依赖 | 支持版本 | +| -------------- | ------------- | +| Vue | `>=3.2.33 <4` | | Ant Design Vue | `>=3.2.20 <4` | 安装时请确保业务项目中的 Vue 与 Ant Design Vue 版本满足上述范围。 @@ -104,12 +104,15 @@ const data = ref({ | `grid` | `WaveformGridOptions` | `{ rowCount: 2, columnCount: 1, showPagination: true }` | 网格和分页 | | `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 | | `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 | +| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 | +| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 | | `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 | | `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID | | `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 | 所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、 -`WaveformAnnotation`、`WaveformRenderingOptions` 和 `WaveformGridOptions`。 +`WaveformAnnotation`、`WaveformRenderingOptions`、`WaveformZeroLineOptions` 和 +`WaveformGridOptions`。 ### 数据结构 @@ -381,6 +384,35 @@ const hiddenSeriesIds = ref([]) 显隐状态以规范化后的 `series.id` 为键。要在数据刷新和重新排序后稳定保留状态,每个系列都应 提供全图唯一且稳定的显式 `id`;自动生成的索引 ID 或重复 ID 添加的后缀不保证跨排序稳定。 +### 零值参考线与净图 + +`zeroLine` 用于绘制 `y = 0` 的水平参考线,默认隐藏。参考线只在对应 Y 轴的当前 domain +包含 0 时渲染,不会为了显示参考线而扩展数据范围。多值轴模式下,每根可见 Y 轴分别按自身 +scale 定位零线: + +```vue + +``` + +`dash` 直接对应 SVG 的 `stroke-dasharray`;传入空字符串可显示实线。无效或非正数的 +`width` 会回退到 `1`。 + +设置 `cleanView` 后,组件隐藏标题、图例、网格、坐标轴、轴标签、图框背景与边框、帧水印、 +零值参考线、标注和分页器,并取消这些元素预留的边距,仅保留波形数据层。缩放、悬浮、十字线和 +tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失: + +```vue + +``` + ### 网格、分页与交互模式 `grid` 控制独立图框的行列数(范围 `1–10`)以及是否显示分页器。默认值为 `2` 行、 diff --git a/src/App.test.ts b/src/App.test.ts index 84052ed..d9f4f55 100644 --- a/src/App.test.ts +++ b/src/App.test.ts @@ -41,6 +41,12 @@ describe('App workspace layout', { timeout: 20_000 }, () => { expect(panel.find('[aria-label="波形展示方式"]').exists()).toBe(true) expect(panel.find('[aria-label="波形叠加方式"]').exists()).toBe(true) expect(panel.find('[aria-label="波形网格尺寸"]').exists()).toBe(true) + expect(panel.find('[aria-label="净图模式"]').exists()).toBe(true) + expect(panel.find('[aria-label="显示零值参考线"]').exists()).toBe(true) + const zeroLineControls = panel.get('.zero-line-controls') + expect(zeroLineControls.findAllComponents(ColorPicker)).toHaveLength(1) + expect(zeroLineControls.find('[aria-label="零值参考线线宽"]').exists()).toBe(true) + expect(zeroLineControls.find('[aria-label="零值参考线线型"]').exists()).toBe(true) expect(frameControls.findAllComponents(ColorPicker)).toHaveLength(2) expect(frameControls.text()).toContain('边框颜色') expect(frameControls.text()).toContain('背景颜色') @@ -71,6 +77,23 @@ describe('App workspace layout', { timeout: 20_000 }, () => { wrapper.unmount() }) + it('passes clean view and zero-line controls to the chart', async () => { + const wrapper = mount(App) + await flushPromises() + const chart = wrapper.getComponent(WaveformChart) + + expect(chart.props('cleanView')).toBe(false) + expect(chart.props('zeroLine')).toMatchObject({ visible: false, color: '#98a2b3', width: 1 }) + + await wrapper.get('[aria-label="净图模式"]').trigger('click') + await wrapper.get('[aria-label="显示零值参考线"]').trigger('click') + await flushPromises() + + expect(chart.props('cleanView')).toBe(true) + expect(chart.props('zeroLine')).toMatchObject({ visible: true, color: '#98a2b3', width: 1 }) + wrapper.unmount() + }) + it('switches overlaid tracks between single-axis and multi-axis rendering', async () => { const wrapper = mount(App) await flushPromises() diff --git a/src/App.vue b/src/App.vue index f4cce8a..d1a3dee 100644 --- a/src/App.vue +++ b/src/App.vue @@ -17,6 +17,7 @@ import { type WaveformSeries, type WaveformTitleOptions, type WaveformZoomEndPayload, + type WaveformZeroLineOptions, } from './components' import chartWaveformsJson from './data/chartWaveforms.json' import demoWaveformsJson from './data/demoWaveforms.json' @@ -53,6 +54,11 @@ const frameBackgroundColor = ref('rgba(255, 255, 255, 0)') const frameWatermarkVisible = ref(true) const annotations = ref([]) const annotationsVisible = ref(true) +const cleanView = ref(false) +const zeroLineVisible = ref(false) +const zeroLineColor = ref('#98a2b3') +const zeroLineWidth = ref(1) +const zeroLineDash = ref('6 4') const interactionMode = ref('zoom') const legendPosition = ref('top-right') const legendOrientation = ref('auto') @@ -88,6 +94,11 @@ const frameBorderStyleOptions = [ { label: '实线', value: 'solid' }, { label: '虚线', value: 'dashed' }, ] +const zeroLineDashOptions = [ + { label: '虚线', value: '6 4' }, + { label: '点划线', value: '2 3' }, + { label: '实线', value: '' }, +] const titleAlignOptions: Array<{ label: string value: NonNullable @@ -109,6 +120,12 @@ const frameStyle = computed(() => ({ borderStyle: frameBorderStyle.value, backgroundColor: frameBackgroundColor.value, })) +const zeroLine = computed(() => ({ + visible: zeroLineVisible.value, + color: zeroLineColor.value, + width: zeroLineWidth.value, + dash: zeroLineDash.value, +})) const seriesStylePresets: Array> = [ { lineType: 'none', pointType: 'triangle', errorBar: { visible: true } }, @@ -353,6 +370,53 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)

视图

+
+ +
+
+ +
+
+

零值参考线

+ +
+
+ + +