feat(chart): add zero line and clean view
This commit is contained in:
36
README.md
36
README.md
@@ -40,7 +40,7 @@ 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` |
|
| Ant Design Vue | `>=3.2.20 <4` |
|
||||||
|
|
||||||
@@ -104,12 +104,15 @@ const data = ref<WaveformData>({
|
|||||||
| `grid` | `WaveformGridOptions` | `{ rowCount: 2, columnCount: 1, showPagination: true }` | 网格和分页 |
|
| `grid` | `WaveformGridOptions` | `{ rowCount: 2, columnCount: 1, showPagination: true }` | 网格和分页 |
|
||||||
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
||||||
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
||||||
|
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
||||||
|
| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 |
|
||||||
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
||||||
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
||||||
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
||||||
|
|
||||||
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
||||||
`WaveformAnnotation`、`WaveformRenderingOptions` 和 `WaveformGridOptions`。
|
`WaveformAnnotation`、`WaveformRenderingOptions`、`WaveformZeroLineOptions` 和
|
||||||
|
`WaveformGridOptions`。
|
||||||
|
|
||||||
### 数据结构
|
### 数据结构
|
||||||
|
|
||||||
@@ -381,6 +384,35 @@ const hiddenSeriesIds = ref<string[]>([])
|
|||||||
显隐状态以规范化后的 `series.id` 为键。要在数据刷新和重新排序后稳定保留状态,每个系列都应
|
显隐状态以规范化后的 `series.id` 为键。要在数据刷新和重新排序后稳定保留状态,每个系列都应
|
||||||
提供全图唯一且稳定的显式 `id`;自动生成的索引 ID 或重复 ID 添加的后缀不保证跨排序稳定。
|
提供全图唯一且稳定的显式 `id`;自动生成的索引 ID 或重复 ID 添加的后缀不保证跨排序稳定。
|
||||||
|
|
||||||
|
### 零值参考线与净图
|
||||||
|
|
||||||
|
`zeroLine` 用于绘制 `y = 0` 的水平参考线,默认隐藏。参考线只在对应 Y 轴的当前 domain
|
||||||
|
包含 0 时渲染,不会为了显示参考线而扩展数据范围。多值轴模式下,每根可见 Y 轴分别按自身
|
||||||
|
scale 定位零线:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<WaveformChart
|
||||||
|
:data="chartData"
|
||||||
|
:zero-line="{
|
||||||
|
visible: true,
|
||||||
|
color: '#98a2b3',
|
||||||
|
width: 1,
|
||||||
|
dash: '6 4',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
`dash` 直接对应 SVG 的 `stroke-dasharray`;传入空字符串可显示实线。无效或非正数的
|
||||||
|
`width` 会回退到 `1`。
|
||||||
|
|
||||||
|
设置 `cleanView` 后,组件隐藏标题、图例、网格、坐标轴、轴标签、图框背景与边框、帧水印、
|
||||||
|
零值参考线、标注和分页器,并取消这些元素预留的边距,仅保留波形数据层。缩放、悬浮、十字线和
|
||||||
|
tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<WaveformChart :data="chartData" :clean-view="cleanViewEnabled" />
|
||||||
|
```
|
||||||
|
|
||||||
### 网格、分页与交互模式
|
### 网格、分页与交互模式
|
||||||
|
|
||||||
`grid` 控制独立图框的行列数(范围 `1–10`)以及是否显示分页器。默认值为 `2` 行、
|
`grid` 控制独立图框的行列数(范围 `1–10`)以及是否显示分页器。默认值为 `2` 行、
|
||||||
|
|||||||
@@ -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)
|
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.findAllComponents(ColorPicker)).toHaveLength(2)
|
||||||
expect(frameControls.text()).toContain('边框颜色')
|
expect(frameControls.text()).toContain('边框颜色')
|
||||||
expect(frameControls.text()).toContain('背景颜色')
|
expect(frameControls.text()).toContain('背景颜色')
|
||||||
@@ -71,6 +77,23 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
wrapper.unmount()
|
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 () => {
|
it('switches overlaid tracks between single-axis and multi-axis rendering', async () => {
|
||||||
const wrapper = mount(App)
|
const wrapper = mount(App)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
66
src/App.vue
66
src/App.vue
@@ -17,6 +17,7 @@ import {
|
|||||||
type WaveformSeries,
|
type WaveformSeries,
|
||||||
type WaveformTitleOptions,
|
type WaveformTitleOptions,
|
||||||
type WaveformZoomEndPayload,
|
type WaveformZoomEndPayload,
|
||||||
|
type WaveformZeroLineOptions,
|
||||||
} from './components'
|
} from './components'
|
||||||
import chartWaveformsJson from './data/chartWaveforms.json'
|
import chartWaveformsJson from './data/chartWaveforms.json'
|
||||||
import demoWaveformsJson from './data/demoWaveforms.json'
|
import demoWaveformsJson from './data/demoWaveforms.json'
|
||||||
@@ -53,6 +54,11 @@ const frameBackgroundColor = ref('rgba(255, 255, 255, 0)')
|
|||||||
const frameWatermarkVisible = ref(true)
|
const frameWatermarkVisible = ref(true)
|
||||||
const annotations = ref<WaveformAnnotation[]>([])
|
const annotations = ref<WaveformAnnotation[]>([])
|
||||||
const annotationsVisible = ref(true)
|
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<WaveformInteractionMode>('zoom')
|
const interactionMode = ref<WaveformInteractionMode>('zoom')
|
||||||
const legendPosition = ref<WaveformLegendPosition>('top-right')
|
const legendPosition = ref<WaveformLegendPosition>('top-right')
|
||||||
const legendOrientation = ref<WaveformLegendOrientation>('auto')
|
const legendOrientation = ref<WaveformLegendOrientation>('auto')
|
||||||
@@ -88,6 +94,11 @@ const frameBorderStyleOptions = [
|
|||||||
{ label: '实线', value: 'solid' },
|
{ label: '实线', value: 'solid' },
|
||||||
{ label: '虚线', value: 'dashed' },
|
{ label: '虚线', value: 'dashed' },
|
||||||
]
|
]
|
||||||
|
const zeroLineDashOptions = [
|
||||||
|
{ label: '虚线', value: '6 4' },
|
||||||
|
{ label: '点划线', value: '2 3' },
|
||||||
|
{ label: '实线', value: '' },
|
||||||
|
]
|
||||||
const titleAlignOptions: Array<{
|
const titleAlignOptions: Array<{
|
||||||
label: string
|
label: string
|
||||||
value: NonNullable<WaveformTitleOptions['align']>
|
value: NonNullable<WaveformTitleOptions['align']>
|
||||||
@@ -109,6 +120,12 @@ const frameStyle = computed<WaveformFrameStyle>(() => ({
|
|||||||
borderStyle: frameBorderStyle.value,
|
borderStyle: frameBorderStyle.value,
|
||||||
backgroundColor: frameBackgroundColor.value,
|
backgroundColor: frameBackgroundColor.value,
|
||||||
}))
|
}))
|
||||||
|
const zeroLine = computed<WaveformZeroLineOptions>(() => ({
|
||||||
|
visible: zeroLineVisible.value,
|
||||||
|
color: zeroLineColor.value,
|
||||||
|
width: zeroLineWidth.value,
|
||||||
|
dash: zeroLineDash.value,
|
||||||
|
}))
|
||||||
|
|
||||||
const seriesStylePresets: Array<Pick<WaveformSeries, 'lineType' | 'pointType' | 'errorBar'>> = [
|
const seriesStylePresets: Array<Pick<WaveformSeries, 'lineType' | 'pointType' | 'errorBar'>> = [
|
||||||
{ lineType: 'none', pointType: 'triangle', errorBar: { visible: true } },
|
{ lineType: 'none', pointType: 'triangle', errorBar: { visible: true } },
|
||||||
@@ -353,6 +370,53 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
|
|||||||
<section class="control-section">
|
<section class="control-section">
|
||||||
<h2>视图</h2>
|
<h2>视图</h2>
|
||||||
<Button block aria-label="重置波形视图" @click="resetWaveformViewport">重置视图</Button>
|
<Button block aria-label="重置波形视图" @click="resetWaveformViewport">重置视图</Button>
|
||||||
|
<div class="auxiliary-style-controls" style="margin-top: 10px">
|
||||||
|
<label class="frame-style-control frame-style-control--switch">
|
||||||
|
<span>净图</span>
|
||||||
|
<Switch v-model:checked="cleanView" size="small" aria-label="净图模式" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="control-section">
|
||||||
|
<div class="control-section__header">
|
||||||
|
<h2>零值参考线</h2>
|
||||||
|
<Switch v-model:checked="zeroLineVisible" size="small" aria-label="显示零值参考线" />
|
||||||
|
</div>
|
||||||
|
<div class="auxiliary-style-controls zero-line-controls" style="margin-top: 10px">
|
||||||
|
<label class="frame-style-control">
|
||||||
|
<span>颜色</span>
|
||||||
|
<ColorPicker
|
||||||
|
v-model:pure-color="zeroLineColor"
|
||||||
|
aria-label="零值参考线颜色"
|
||||||
|
use-type="pure"
|
||||||
|
picker-type="chrome"
|
||||||
|
format="hex"
|
||||||
|
:disable-alpha="true"
|
||||||
|
:blur-close="true"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="frame-style-control">
|
||||||
|
<span>线宽</span>
|
||||||
|
<InputNumber
|
||||||
|
v-model:value="zeroLineWidth"
|
||||||
|
:min="0.5"
|
||||||
|
:max="10"
|
||||||
|
:step="0.5"
|
||||||
|
size="small"
|
||||||
|
aria-label="零值参考线线宽"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="frame-style-control">
|
||||||
|
<span>线型</span>
|
||||||
|
<Select
|
||||||
|
v-model:value="zeroLineDash"
|
||||||
|
:options="zeroLineDashOptions"
|
||||||
|
size="small"
|
||||||
|
aria-label="零值参考线线型"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="control-section">
|
<section class="control-section">
|
||||||
@@ -602,6 +666,8 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
|
|||||||
interactive: true,
|
interactive: true,
|
||||||
}"
|
}"
|
||||||
:frame-style="frameStyle"
|
:frame-style="frameStyle"
|
||||||
|
:clean-view="cleanView"
|
||||||
|
:zero-line="zeroLine"
|
||||||
:frame-number="frameWatermarkVisible ? 1 : undefined"
|
:frame-number="frameWatermarkVisible ? 1 : undefined"
|
||||||
v-model:annotations="annotations"
|
v-model:annotations="annotations"
|
||||||
v-model:annotations-visible="annotationsVisible"
|
v-model:annotations-visible="annotationsVisible"
|
||||||
|
|||||||
@@ -220,6 +220,152 @@ describe('WaveformChart', () => {
|
|||||||
second.unmount()
|
second.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders a configurable zero line only when the Y domain contains zero', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: -2 },
|
||||||
|
{ x: 1, y: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ zeroLine: { visible: true, color: '#475467', width: 2, dash: '3 2' } },
|
||||||
|
)
|
||||||
|
|
||||||
|
const zeroLine = wrapper.get('.waveform-chart__zero-line')
|
||||||
|
expect(zeroLine.attributes()).toMatchObject({
|
||||||
|
stroke: '#475467',
|
||||||
|
'stroke-width': '2',
|
||||||
|
'stroke-dasharray': '3 2',
|
||||||
|
'data-y-axis-index': '0',
|
||||||
|
})
|
||||||
|
expect(zeroLine.attributes('y1')).toBe(zeroLine.attributes('y2'))
|
||||||
|
|
||||||
|
await wrapper.setProps({ zeroLine: { visible: false } })
|
||||||
|
expect(wrapper.find('.waveform-chart__zero-line').exists()).toBe(false)
|
||||||
|
|
||||||
|
const positive = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 2 },
|
||||||
|
{ x: 1, y: 4 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ zeroLine: { visible: true } },
|
||||||
|
)
|
||||||
|
expect(positive.find('.waveform-chart__zero-line').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('renders zero lines from each visible Y axis in multi-axis mode', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'series',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: 'small',
|
||||||
|
trackId: 'overlay',
|
||||||
|
name: 'small',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
{ x: 1, y: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'large',
|
||||||
|
trackId: 'overlay',
|
||||||
|
name: 'large',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: -10 },
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ overlayMode: 'multi-axis', zeroLine: { visible: true } },
|
||||||
|
)
|
||||||
|
|
||||||
|
const zeroLines = wrapper.findAll('.waveform-chart__zero-line')
|
||||||
|
expect(zeroLines).toHaveLength(2)
|
||||||
|
expect(zeroLines.map((line) => line.attributes('data-y-axis-index'))).toEqual(['0', '1'])
|
||||||
|
expect(zeroLines[0].attributes('y1')).not.toBe(zeroLines[1].attributes('y1'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('uses the full drawing area and hides auxiliary layers in clean view', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'series',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: 'first',
|
||||||
|
trackId: 'overlay',
|
||||||
|
name: 'first',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'second',
|
||||||
|
trackId: 'overlay',
|
||||||
|
name: 'second',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 1 },
|
||||||
|
{ x: 1, y: 2 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'third',
|
||||||
|
name: 'third',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 2 },
|
||||||
|
{ x: 1, y: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cleanView: true,
|
||||||
|
grid: { rowCount: 1, columnCount: 1, showPagination: true },
|
||||||
|
title: { text: 'hidden title' },
|
||||||
|
frameNumber: 1,
|
||||||
|
annotations: [{ id: 'note', seriesId: 'first', x: 0.5, y: 0, text: 'hidden note' }],
|
||||||
|
zeroLine: { visible: true },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(wrapper.get('.waveform-chart').attributes('data-chart-left-margin')).toBe('0')
|
||||||
|
expect(wrapper.get('.waveform-chart__track').attributes('data-track-height')).toBe('360')
|
||||||
|
expect(wrapper.findAll('.waveform-chart__series')).toHaveLength(2)
|
||||||
|
expect(wrapper.find('.waveform-chart__overlay--independent').exists()).toBe(true)
|
||||||
|
expect(wrapper.find('.waveform-chart__title-area').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__axis').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__plot-frame').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__plot-background').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__watermark').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__legend-layer').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__label').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-chart__zero-line').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.waveform-annotation-layer').exists()).toBe(false)
|
||||||
|
expect(wrapper.find('.ant-pagination').exists()).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
it('places start, middle, and end step transitions at the expected X positions', async () => {
|
it('places start, middle, and end step transitions at the expected X positions', async () => {
|
||||||
const lineTypes = ['step-start', 'step-middle', 'step-end', 'step-after'] as const
|
const lineTypes = ['step-start', 'step-middle', 'step-end', 'step-after'] as const
|
||||||
const wrapper = await mountSizedChart({
|
const wrapper = await mountSizedChart({
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
type WaveformPoint,
|
type WaveformPoint,
|
||||||
type WaveformRenderingOptions,
|
type WaveformRenderingOptions,
|
||||||
type WaveformTitleOptions,
|
type WaveformTitleOptions,
|
||||||
|
type WaveformZeroLineOptions,
|
||||||
type WaveformZoomEndPayload,
|
type WaveformZoomEndPayload,
|
||||||
} from './data/types'
|
} from './data/types'
|
||||||
import {
|
import {
|
||||||
@@ -109,6 +110,8 @@ const props = withDefaults(
|
|||||||
legend?: WaveformLegendOptions
|
legend?: WaveformLegendOptions
|
||||||
hiddenSeriesIds?: string[]
|
hiddenSeriesIds?: string[]
|
||||||
defaultHiddenSeriesIds?: string[]
|
defaultHiddenSeriesIds?: string[]
|
||||||
|
cleanView?: boolean
|
||||||
|
zeroLine?: WaveformZeroLineOptions
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
displayMode: 'independent',
|
displayMode: 'independent',
|
||||||
@@ -128,6 +131,8 @@ const props = withDefaults(
|
|||||||
rendering: () => ({}),
|
rendering: () => ({}),
|
||||||
legend: () => ({ position: 'top-right', orientation: 'auto' }),
|
legend: () => ({ position: 'top-right', orientation: 'auto' }),
|
||||||
defaultHiddenSeriesIds: () => [],
|
defaultHiddenSeriesIds: () => [],
|
||||||
|
cleanView: false,
|
||||||
|
zeroLine: () => ({ visible: false }),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -255,6 +260,16 @@ const containerStyle = computed(() => ({
|
|||||||
height: fixedHeight.value === undefined ? '100%' : `${fixedHeight.value}px`,
|
height: fixedHeight.value === undefined ? '100%' : `${fixedHeight.value}px`,
|
||||||
}))
|
}))
|
||||||
const legendPosition = computed<WaveformLegendPosition>(() => props.legend?.position ?? 'top-right')
|
const legendPosition = computed<WaveformLegendPosition>(() => props.legend?.position ?? 'top-right')
|
||||||
|
const isCleanView = computed(() => props.cleanView === true)
|
||||||
|
const resolvedZeroLine = computed(() => {
|
||||||
|
const width = props.zeroLine?.width
|
||||||
|
return {
|
||||||
|
visible: props.zeroLine?.visible === true,
|
||||||
|
color: props.zeroLine?.color || '#98a2b3',
|
||||||
|
width: typeof width === 'number' && Number.isFinite(width) && width > 0 ? width : 1,
|
||||||
|
dash: props.zeroLine?.dash ?? '6 4',
|
||||||
|
}
|
||||||
|
})
|
||||||
const legendBackgroundColor = computed(
|
const legendBackgroundColor = computed(
|
||||||
() => props.legend?.backgroundColor || 'rgba(255, 255, 255, 0.7)',
|
() => props.legend?.backgroundColor || 'rgba(255, 255, 255, 0.7)',
|
||||||
)
|
)
|
||||||
@@ -275,7 +290,10 @@ const legendOrientation = computed<Exclude<WaveformLegendOrientation, 'auto'>>((
|
|||||||
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
||||||
const titleVisible = computed(
|
const titleVisible = computed(
|
||||||
() =>
|
() =>
|
||||||
Boolean(props.title) && props.title?.visible !== false && resolvedTitleText.value.length > 0,
|
!isCleanView.value &&
|
||||||
|
Boolean(props.title) &&
|
||||||
|
props.title?.visible !== false &&
|
||||||
|
resolvedTitleText.value.length > 0,
|
||||||
)
|
)
|
||||||
const titleFontSize = computed(() => {
|
const titleFontSize = computed(() => {
|
||||||
const fontSize = props.title?.textStyle?.fontSize
|
const fontSize = props.title?.textStyle?.fontSize
|
||||||
@@ -326,8 +344,11 @@ const titleLayout = computed(() =>
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
const titleAreaHeight = computed(() => (titleVisible.value ? titleLayout.value.areaHeight : 0))
|
const titleAreaHeight = computed(() => (titleVisible.value ? titleLayout.value.areaHeight : 0))
|
||||||
|
const chartTopMargin = computed(() => (isCleanView.value ? 0 : margin.top))
|
||||||
const drawingHeight = computed(() => Math.max(0, chartHeight.value - titleAreaHeight.value))
|
const drawingHeight = computed(() => Math.max(0, chartHeight.value - titleAreaHeight.value))
|
||||||
const innerHeight = computed(() => Math.max(0, drawingHeight.value - margin.top - margin.bottom))
|
const innerHeight = computed(() =>
|
||||||
|
Math.max(0, drawingHeight.value - (isCleanView.value ? 0 : margin.top + margin.bottom)),
|
||||||
|
)
|
||||||
const titleAreaStyle = computed<CSSProperties>(() => ({
|
const titleAreaStyle = computed<CSSProperties>(() => ({
|
||||||
height: `${titleAreaHeight.value}px`,
|
height: `${titleAreaHeight.value}px`,
|
||||||
justifyContent:
|
justifyContent:
|
||||||
@@ -440,7 +461,7 @@ const hasVisibleWaveformData = computed(() =>
|
|||||||
)
|
)
|
||||||
const chartLeftMargin = computed(() =>
|
const chartLeftMargin = computed(() =>
|
||||||
Math.max(
|
Math.max(
|
||||||
margin.left,
|
isCleanView.value ? 0 : margin.left,
|
||||||
hasYAxisLabels.value
|
hasYAxisLabels.value
|
||||||
? yAxisMetrics.value.fullClearance
|
? yAxisMetrics.value.fullClearance
|
||||||
: hasVisibleWaveformData.value
|
: hasVisibleWaveformData.value
|
||||||
@@ -461,13 +482,19 @@ const multiAxisClearance = computed(() =>
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
const resolvedChartLeftMargin = computed(() =>
|
const resolvedChartLeftMargin = computed(() =>
|
||||||
props.overlayMode === 'multi-axis'
|
isCleanView.value
|
||||||
|
? 0
|
||||||
|
: props.overlayMode === 'multi-axis'
|
||||||
? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
|
? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
|
||||||
: chartLeftMargin.value,
|
: chartLeftMargin.value,
|
||||||
)
|
)
|
||||||
const chartRightMargin = computed(() =>
|
const chartRightMargin = computed(() =>
|
||||||
props.overlayMode === 'multi-axis'
|
props.overlayMode === 'multi-axis'
|
||||||
? Math.max(margin.right, multiAxisClearance.value.right)
|
? isCleanView.value
|
||||||
|
? 0
|
||||||
|
: Math.max(margin.right, multiAxisClearance.value.right)
|
||||||
|
: isCleanView.value
|
||||||
|
? 0
|
||||||
: margin.right,
|
: margin.right,
|
||||||
)
|
)
|
||||||
const innerWidth = computed(() =>
|
const innerWidth = computed(() =>
|
||||||
@@ -568,6 +595,7 @@ const gridCells = computed(() => {
|
|||||||
props.displayMode,
|
props.displayMode,
|
||||||
pagedTracks.value.map(Boolean),
|
pagedTracks.value.map(Boolean),
|
||||||
yAxisLayout.value.horizontalGap,
|
yAxisLayout.value.horizontalGap,
|
||||||
|
!isCleanView.value,
|
||||||
)
|
)
|
||||||
return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] }))
|
return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] }))
|
||||||
})
|
})
|
||||||
@@ -593,7 +621,7 @@ const trackLayouts = computed<TrackLayout[]>(() =>
|
|||||||
: sharedYDomains.value,
|
: sharedYDomains.value,
|
||||||
timeUnit: props.timeUnit,
|
timeUnit: props.timeUnit,
|
||||||
rendering: renderingOptions.value,
|
rendering: renderingOptions.value,
|
||||||
hideSecondaryLabels: yAxisLayout.value.hideSecondaryLabels,
|
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
||||||
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
||||||
showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
|
showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
|
||||||
}),
|
}),
|
||||||
@@ -1004,7 +1032,7 @@ function resolvePointerEditorAnchor(
|
|||||||
const track = trackIndex === undefined ? undefined : trackLayouts.value[trackIndex]
|
const track = trackIndex === undefined ? undefined : trackLayouts.value[trackIndex]
|
||||||
return {
|
return {
|
||||||
x: resolvedChartLeftMargin.value + (track ? track.left + pointerX : pointerX),
|
x: resolvedChartLeftMargin.value + (track ? track.left + pointerX : pointerX),
|
||||||
y: titleAreaHeight.value + margin.top + (track ? track.top + pointerY : pointerY),
|
y: titleAreaHeight.value + chartTopMargin.value + (track ? track.top + pointerY : pointerY),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1018,7 +1046,7 @@ function resolveAnnotationEditorAnchor(annotation: WaveformAnnotation): Annotati
|
|||||||
: chartWidth.value / 2,
|
: chartWidth.value / 2,
|
||||||
y: track
|
y: track
|
||||||
? titleAreaHeight.value +
|
? titleAreaHeight.value +
|
||||||
margin.top +
|
chartTopMargin.value +
|
||||||
track.top +
|
track.top +
|
||||||
resolveSeriesYScale(track, annotation.seriesId)(annotation.y)
|
resolveSeriesYScale(track, annotation.seriesId)(annotation.y)
|
||||||
: chartHeight.value / 2,
|
: chartHeight.value / 2,
|
||||||
@@ -1293,7 +1321,7 @@ function handleIndependentPointerMove(event: PointerEvent, trackIndex: number) {
|
|||||||
})
|
})
|
||||||
commitHover(nextPoints, trackIndex, {
|
commitHover(nextPoints, trackIndex, {
|
||||||
x: resolvedChartLeftMargin.value + track.left + pointerX,
|
x: resolvedChartLeftMargin.value + track.left + pointerX,
|
||||||
y: titleAreaHeight.value + margin.top + track.top + pointerY,
|
y: titleAreaHeight.value + chartTopMargin.value + track.top + pointerY,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1324,7 +1352,7 @@ function handleSharedPointerMove(event: PointerEvent) {
|
|||||||
)
|
)
|
||||||
commitHover(nextPoints, null, {
|
commitHover(nextPoints, null, {
|
||||||
x: resolvedChartLeftMargin.value + pointerX,
|
x: resolvedChartLeftMargin.value + pointerX,
|
||||||
y: titleAreaHeight.value + margin.top + pointerY,
|
y: titleAreaHeight.value + chartTopMargin.value + pointerY,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -1842,8 +1870,12 @@ onBeforeUnmount(() => {
|
|||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
|
|
||||||
<g :transform="`translate(${resolvedChartLeftMargin}, ${margin.top})`">
|
<g :transform="`translate(${resolvedChartLeftMargin}, ${chartTopMargin})`">
|
||||||
<g v-if="displayMode !== 'compact'" class="waveform-chart__grid-slots" aria-hidden="true">
|
<g
|
||||||
|
v-if="displayMode !== 'compact' && !isCleanView"
|
||||||
|
class="waveform-chart__grid-slots"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
<g
|
<g
|
||||||
v-for="cell in gridCells"
|
v-for="cell in gridCells"
|
||||||
:key="`grid-slot-${cell.slotIndex}`"
|
:key="`grid-slot-${cell.slotIndex}`"
|
||||||
@@ -1889,6 +1921,8 @@ onBeforeUnmount(() => {
|
|||||||
:interaction-mode="activeInteractionMode"
|
:interaction-mode="activeInteractionMode"
|
||||||
:frame-number="resolveFrameNumber(track.index)"
|
:frame-number="resolveFrameNumber(track.index)"
|
||||||
:frame-style="frameStyle"
|
:frame-style="frameStyle"
|
||||||
|
:clean-view="isCleanView"
|
||||||
|
:zero-line="resolvedZeroLine"
|
||||||
:time-unit="timeUnit"
|
:time-unit="timeUnit"
|
||||||
:y-label="yLabel"
|
:y-label="yLabel"
|
||||||
:hovered-point="hoveredSeriesPoints.find((p) => p.trackIndex === track.index)"
|
:hovered-point="hoveredSeriesPoints.find((p) => p.trackIndex === track.index)"
|
||||||
@@ -1912,6 +1946,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<WaveformAnnotationLayer
|
<WaveformAnnotationLayer
|
||||||
|
v-if="!isCleanView"
|
||||||
:annotations="renderedAnnotations"
|
:annotations="renderedAnnotations"
|
||||||
:visible="annotationsVisible"
|
:visible="annotationsVisible"
|
||||||
@contextmenu="handleExistingAnnotationContextMenu"
|
@contextmenu="handleExistingAnnotationContextMenu"
|
||||||
@@ -1920,7 +1955,7 @@ onBeforeUnmount(() => {
|
|||||||
@drag-end="endAnnotationDrag"
|
@drag-end="endAnnotationDrag"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<g class="waveform-chart__legend-layer">
|
<g v-if="!isCleanView" class="waveform-chart__legend-layer">
|
||||||
<g
|
<g
|
||||||
v-for="track in trackLayouts"
|
v-for="track in trackLayouts"
|
||||||
:key="`legend-${track.index}-${track.series.name}`"
|
:key="`legend-${track.index}-${track.series.name}`"
|
||||||
@@ -1944,7 +1979,7 @@ onBeforeUnmount(() => {
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<text
|
<text
|
||||||
v-if="resolvedXLabel"
|
v-if="resolvedXLabel && !isCleanView"
|
||||||
class="waveform-chart__label"
|
class="waveform-chart__label"
|
||||||
:x="innerWidth / 2"
|
:x="innerWidth / 2"
|
||||||
:y="xAxisTitleY"
|
:y="xAxisTitleY"
|
||||||
@@ -1966,7 +2001,7 @@ onBeforeUnmount(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="gridOptions.showPagination && pageCount > 1"
|
v-if="gridOptions.showPagination && pageCount > 1 && !isCleanView"
|
||||||
class="waveform-chart__pagination"
|
class="waveform-chart__pagination"
|
||||||
aria-label="波形分页"
|
aria-label="波形分页"
|
||||||
:current="currentPage"
|
:current="currentPage"
|
||||||
@@ -1978,7 +2013,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<WaveformAnnotationToolbar
|
<WaveformAnnotationToolbar
|
||||||
v-if="showAnnotationToolbar"
|
v-if="showAnnotationToolbar && !isCleanView"
|
||||||
:interaction-mode="activeInteractionMode"
|
:interaction-mode="activeInteractionMode"
|
||||||
:annotations-visible="annotationsVisible"
|
:annotations-visible="annotationsVisible"
|
||||||
@update:interaction-mode="setInteractionMode"
|
@update:interaction-mode="setInteractionMode"
|
||||||
@@ -1986,7 +2021,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<WaveformAnnotationEditor
|
<WaveformAnnotationEditor
|
||||||
v-if="annotationInteraction.editorDraft.value"
|
v-if="annotationInteraction.editorDraft.value && !isCleanView"
|
||||||
:annotation="annotationInteraction.editorDraft.value.annotation"
|
:annotation="annotationInteraction.editorDraft.value.annotation"
|
||||||
:mode="annotationInteraction.editorDraft.value.mode"
|
:mode="annotationInteraction.editorDraft.value.mode"
|
||||||
:series="editorSeries"
|
:series="editorSeries"
|
||||||
@@ -1998,6 +2033,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<WaveformAnnotationContextMenu
|
<WaveformAnnotationContextMenu
|
||||||
|
v-if="!isCleanView"
|
||||||
:visible="annotationInteraction.contextMenu.value !== null"
|
:visible="annotationInteraction.contextMenu.value !== null"
|
||||||
:x="annotationInteraction.contextMenu.value?.x || 0"
|
:x="annotationInteraction.contextMenu.value?.x || 0"
|
||||||
:y="annotationInteraction.contextMenu.value?.y || 0"
|
:y="annotationInteraction.contextMenu.value?.y || 0"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ export function resolveGridCellGeometry(
|
|||||||
displayMode: WaveformDisplayMode,
|
displayMode: WaveformDisplayMode,
|
||||||
slotHasSeries: boolean[] = [],
|
slotHasSeries: boolean[] = [],
|
||||||
horizontalGap?: number,
|
horizontalGap?: number,
|
||||||
|
showXAxis = true,
|
||||||
): GridCellGeometry[] {
|
): GridCellGeometry[] {
|
||||||
const defaultGap = getGridGap(displayMode)
|
const defaultGap = getGridGap(displayMode)
|
||||||
const columnGap = Number.isFinite(horizontalGap)
|
const columnGap = Number.isFinite(horizontalGap)
|
||||||
@@ -81,7 +82,9 @@ export function resolveGridCellGeometry(
|
|||||||
: defaultGap
|
: defaultGap
|
||||||
const totalHorizontalGap = Math.max(0, options.columnCount - 1) * columnGap
|
const totalHorizontalGap = Math.max(0, options.columnCount - 1) * columnGap
|
||||||
const axisRows = new Set<number>()
|
const axisRows = new Set<number>()
|
||||||
if (displayMode === 'independent') {
|
if (!showXAxis) {
|
||||||
|
// Net view uses the full drawing area for waveform pixels.
|
||||||
|
} else if (displayMode === 'independent') {
|
||||||
for (let row = 0; row < options.rowCount; row += 1) axisRows.add(row)
|
for (let row = 0; row < options.rowCount; row += 1) axisRows.add(row)
|
||||||
} else if (displayMode === 'compact') {
|
} else if (displayMode === 'compact') {
|
||||||
// Compact tracks share one continuous plot stack. Reserve the X-axis band
|
// Compact tracks share one continuous plot stack. Reserve the X-axis band
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformZeroLineOptions,
|
||||||
SingleWaveformData,
|
SingleWaveformData,
|
||||||
WaveformLineType,
|
WaveformLineType,
|
||||||
WaveformPointType,
|
WaveformPointType,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformZeroLineOptions,
|
||||||
WaveformPoint,
|
WaveformPoint,
|
||||||
WaveformSeries,
|
WaveformSeries,
|
||||||
WaveformLineType,
|
WaveformLineType,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
import { computed, nextTick, onMounted, ref, watch } from 'vue'
|
||||||
import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
import { axisBottom, axisLeft, axisRight, select } from 'd3'
|
||||||
import { formatAxisTime, formatScientificAxisLabel } from '../../utils'
|
import { formatAxisTime, formatScientificAxisLabel } from '../../utils'
|
||||||
import type { WaveformFrameStyle } from '../../types'
|
import type { WaveformFrameStyle, WaveformZeroLineOptions } from '../../types'
|
||||||
import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types'
|
import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types'
|
||||||
import type {
|
import type {
|
||||||
DisplaySeries,
|
DisplaySeries,
|
||||||
@@ -37,6 +37,12 @@ interface Props {
|
|||||||
hoveredPoint?: HoveredSeriesPoint
|
hoveredPoint?: HoveredSeriesPoint
|
||||||
/** Y 轴标签回退值 */
|
/** Y 轴标签回退值 */
|
||||||
yLabel?: string
|
yLabel?: string
|
||||||
|
/** Hide visual aids while keeping chart interaction active. */
|
||||||
|
cleanView?: boolean
|
||||||
|
/** Resolved zero reference line style. */
|
||||||
|
zeroLine?: Required<Pick<WaveformZeroLineOptions, 'color' | 'width' | 'dash'>> & {
|
||||||
|
visible: boolean
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Emits {
|
interface Emits {
|
||||||
@@ -51,6 +57,8 @@ interface Emits {
|
|||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
interactionMode: 'zoom',
|
interactionMode: 'zoom',
|
||||||
|
cleanView: false,
|
||||||
|
zeroLine: () => ({ visible: false, color: '#98a2b3', width: 1, dash: '6 4' }),
|
||||||
})
|
})
|
||||||
const emit = defineEmits<Emits>()
|
const emit = defineEmits<Emits>()
|
||||||
|
|
||||||
@@ -111,6 +119,12 @@ function hasCrosshair(): boolean {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function zeroLineY(axis: WaveformYAxisLayout): number | null {
|
||||||
|
const [minimum, maximum] = axis.scale.domain()
|
||||||
|
if (!props.zeroLine.visible || minimum > 0 || maximum < 0) return null
|
||||||
|
return axis.scale(0)
|
||||||
|
}
|
||||||
|
|
||||||
function renderAxes() {
|
function renderAxes() {
|
||||||
props.track.yAxes.forEach((axis, index) => {
|
props.track.yAxes.forEach((axis, index) => {
|
||||||
const element = yAxisElements.value[index]
|
const element = yAxisElements.value[index]
|
||||||
@@ -180,7 +194,7 @@ watch(
|
|||||||
:transform="`translate(${track.left ?? 0}, ${track.top})`"
|
:transform="`translate(${track.left ?? 0}, ${track.top})`"
|
||||||
>
|
>
|
||||||
<rect
|
<rect
|
||||||
v-if="!track.isEmpty"
|
v-if="!track.isEmpty && !cleanView"
|
||||||
class="waveform-track__plot-background waveform-chart__plot-background"
|
class="waveform-track__plot-background waveform-chart__plot-background"
|
||||||
:width="track.width ?? innerWidth"
|
:width="track.width ?? innerWidth"
|
||||||
:height="track.height"
|
:height="track.height"
|
||||||
@@ -190,7 +204,7 @@ watch(
|
|||||||
|
|
||||||
<!-- 网格和背景 -->
|
<!-- 网格和背景 -->
|
||||||
<g
|
<g
|
||||||
v-if="!track.isEmpty && track.hasVisibleSeries"
|
v-if="!track.isEmpty && track.hasVisibleSeries && !cleanView"
|
||||||
:clip-path="`url(#${clipPathId}-${track.index})`"
|
:clip-path="`url(#${clipPathId}-${track.index})`"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
@@ -236,9 +250,31 @@ watch(
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
|
|
||||||
|
<g
|
||||||
|
v-if="!track.isEmpty && track.hasVisibleSeries && zeroLine.visible && !cleanView"
|
||||||
|
class="waveform-track__zero-lines waveform-chart__zero-lines"
|
||||||
|
:clip-path="`url(#${clipPathId}-${track.index})`"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<template v-for="axis in track.yAxes" :key="`zero-line-${track.index}-${axis.index}`">
|
||||||
|
<line
|
||||||
|
v-if="zeroLineY(axis) !== null"
|
||||||
|
class="waveform-track__zero-line waveform-chart__zero-line"
|
||||||
|
:data-y-axis-index="axis.index"
|
||||||
|
x1="0"
|
||||||
|
:x2="track.width ?? innerWidth"
|
||||||
|
:y1="zeroLineY(axis) ?? 0"
|
||||||
|
:y2="zeroLineY(axis) ?? 0"
|
||||||
|
:stroke="zeroLine.color"
|
||||||
|
:stroke-width="zeroLine.width"
|
||||||
|
:stroke-dasharray="zeroLine.dash || undefined"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</g>
|
||||||
|
|
||||||
<!-- 帧编号水印 -->
|
<!-- 帧编号水印 -->
|
||||||
<text
|
<text
|
||||||
v-if="!track.isEmpty && track.hasVisibleSeries && frameNumber !== undefined"
|
v-if="!track.isEmpty && track.hasVisibleSeries && frameNumber !== undefined && !cleanView"
|
||||||
class="waveform-track__watermark waveform-chart__watermark"
|
class="waveform-track__watermark waveform-chart__watermark"
|
||||||
:x="(track.width ?? innerWidth) / 2"
|
:x="(track.width ?? innerWidth) / 2"
|
||||||
:y="track.height / 2"
|
:y="track.height / 2"
|
||||||
@@ -252,13 +288,13 @@ watch(
|
|||||||
|
|
||||||
<!-- X 轴 -->
|
<!-- X 轴 -->
|
||||||
<g
|
<g
|
||||||
v-if="track.showXAxis"
|
v-if="track.showXAxis && !cleanView"
|
||||||
ref="xAxisElement"
|
ref="xAxisElement"
|
||||||
class="waveform-track__axis waveform-track__axis--x waveform-chart__axis waveform-chart__axis--x"
|
class="waveform-track__axis waveform-track__axis--x waveform-chart__axis waveform-chart__axis--x"
|
||||||
:transform="`translate(0, ${track.height})`"
|
:transform="`translate(0, ${track.height})`"
|
||||||
/>
|
/>
|
||||||
<g
|
<g
|
||||||
v-if="track.showXAxis"
|
v-if="track.showXAxis && !cleanView"
|
||||||
class="waveform-track__axis-endpoints waveform-chart__axis-endpoints"
|
class="waveform-track__axis-endpoints waveform-chart__axis-endpoints"
|
||||||
:transform="`translate(0, ${track.height})`"
|
:transform="`translate(0, ${track.height})`"
|
||||||
font-family="sans-serif"
|
font-family="sans-serif"
|
||||||
@@ -285,7 +321,7 @@ watch(
|
|||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<text
|
<text
|
||||||
v-if="track.showXAxis && track.xAxisExponent"
|
v-if="track.showXAxis && track.xAxisExponent && !cleanView"
|
||||||
class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
|
class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
|
||||||
:x="track.width ?? innerWidth"
|
:x="track.width ?? innerWidth"
|
||||||
:y="track.height + 27"
|
:y="track.height + 27"
|
||||||
@@ -297,7 +333,7 @@ watch(
|
|||||||
|
|
||||||
<!-- Y 轴 -->
|
<!-- Y 轴 -->
|
||||||
<g
|
<g
|
||||||
v-for="axis in track.isEmpty ? [] : track.yAxes"
|
v-for="axis in track.isEmpty || cleanView ? [] : track.yAxes"
|
||||||
:key="`y-axis-${track.index}-${axis.index}`"
|
:key="`y-axis-${track.index}-${axis.index}`"
|
||||||
:ref="(element) => setYAxisElement(element, axis.index)"
|
:ref="(element) => setYAxisElement(element, axis.index)"
|
||||||
class="waveform-track__axis waveform-track__axis--y waveform-chart__axis waveform-chart__axis--y"
|
class="waveform-track__axis waveform-track__axis--y waveform-chart__axis waveform-chart__axis--y"
|
||||||
@@ -307,7 +343,9 @@ watch(
|
|||||||
:transform="`translate(${axis.x}, 0)`"
|
:transform="`translate(${axis.x}, 0)`"
|
||||||
/>
|
/>
|
||||||
<text
|
<text
|
||||||
v-for="axis in track.isEmpty ? [] : track.yAxes.filter((item) => item.exponentLabel)"
|
v-for="axis in track.isEmpty || cleanView
|
||||||
|
? []
|
||||||
|
: track.yAxes.filter((item) => item.exponentLabel)"
|
||||||
:key="`y-axis-exponent-${track.index}-${axis.index}`"
|
:key="`y-axis-exponent-${track.index}-${axis.index}`"
|
||||||
class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
|
class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
|
||||||
:data-y-axis-index="axis.index"
|
:data-y-axis-index="axis.index"
|
||||||
@@ -323,6 +361,7 @@ watch(
|
|||||||
<!-- Y 轴标签 -->
|
<!-- Y 轴标签 -->
|
||||||
<g
|
<g
|
||||||
v-if="
|
v-if="
|
||||||
|
!cleanView &&
|
||||||
!track.isEmpty &&
|
!track.isEmpty &&
|
||||||
track.hasVisibleSeries &&
|
track.hasVisibleSeries &&
|
||||||
track.seriesList.length === 1 &&
|
track.seriesList.length === 1 &&
|
||||||
@@ -351,7 +390,7 @@ watch(
|
|||||||
</g>
|
</g>
|
||||||
|
|
||||||
<g
|
<g
|
||||||
v-for="axis in track.yAxes.length > 1 ? track.yAxes.filter(hasYAxisTitle) : []"
|
v-for="axis in !cleanView && track.yAxes.length > 1 ? track.yAxes.filter(hasYAxisTitle) : []"
|
||||||
:key="`y-axis-title-${track.index}-${axis.index}`"
|
:key="`y-axis-title-${track.index}-${axis.index}`"
|
||||||
class="waveform-track__multi-axis-title"
|
class="waveform-track__multi-axis-title"
|
||||||
:data-y-axis-title-index="axis.index"
|
:data-y-axis-title-index="axis.index"
|
||||||
@@ -377,7 +416,7 @@ watch(
|
|||||||
|
|
||||||
<!-- 轨道边框 -->
|
<!-- 轨道边框 -->
|
||||||
<rect
|
<rect
|
||||||
v-if="!track.isEmpty"
|
v-if="!track.isEmpty && !cleanView"
|
||||||
class="waveform-track__plot-frame waveform-chart__plot-frame"
|
class="waveform-track__plot-frame waveform-chart__plot-frame"
|
||||||
:width="track.width ?? innerWidth"
|
:width="track.width ?? innerWidth"
|
||||||
:height="track.height"
|
:height="track.height"
|
||||||
@@ -421,7 +460,7 @@ watch(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<text
|
<text
|
||||||
v-if="!track.isEmpty && !track.hasVisibleSeries"
|
v-if="!track.isEmpty && !track.hasVisibleSeries && !cleanView"
|
||||||
class="waveform-track__no-visible-series"
|
class="waveform-track__no-visible-series"
|
||||||
:x="(track.width ?? innerWidth) / 2"
|
:x="(track.width ?? innerWidth) / 2"
|
||||||
:y="track.height / 2"
|
:y="track.height / 2"
|
||||||
@@ -516,6 +555,11 @@ watch(
|
|||||||
stroke-dasharray: 4 3;
|
stroke-dasharray: 4 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.waveform-track__zero-line {
|
||||||
|
fill: none;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.waveform-track__axis-endpoint {
|
.waveform-track__axis-endpoint {
|
||||||
fill: #667085;
|
fill: #667085;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformZeroLineOptions,
|
||||||
// 数据类型
|
// 数据类型
|
||||||
SingleWaveformData,
|
SingleWaveformData,
|
||||||
WaveformLineType,
|
WaveformLineType,
|
||||||
|
|||||||
@@ -156,7 +156,8 @@ body {
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.frame-style-controls {
|
.frame-style-controls,
|
||||||
|
.auxiliary-style-controls {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,3 +118,11 @@ export interface WaveformFrameStyle {
|
|||||||
borderStyle?: 'solid' | 'dashed'
|
borderStyle?: 'solid' | 'dashed'
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Styling and visibility options for the horizontal zero-value reference line. */
|
||||||
|
export interface WaveformZeroLineOptions {
|
||||||
|
visible?: boolean
|
||||||
|
color?: string
|
||||||
|
width?: number
|
||||||
|
dash?: string
|
||||||
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformZeroLineOptions,
|
||||||
} from './chart'
|
} from './chart'
|
||||||
|
|
||||||
// 数据类型
|
// 数据类型
|
||||||
|
|||||||
Reference in New Issue
Block a user