Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc0cc2e0d7 | ||
|
|
3ed17eb061 | ||
|
|
c258c11795 |
106
README.md
106
README.md
@@ -3,6 +3,8 @@
|
|||||||
基于 Vue 3、TypeScript 和 D3 的响应式 SVG 波形图组件。适合展示单通道、多通道和大规模采样
|
基于 Vue 3、TypeScript 和 D3 的响应式 SVG 波形图组件。适合展示单通道、多通道和大规模采样
|
||||||
数据,内置缩放、tooltip、图例、误差棒、标注、分页和多 Y 轴叠加。
|
数据,内置缩放、tooltip、图例、误差棒、标注、分页和多 Y 轴叠加。
|
||||||
|
|
||||||
|
当前稳定版本:`v0.1.15`。
|
||||||
|
|
||||||
组件使用不可变数据模型:替换 `data` 引用后会重新计算数据域和视口;大数据会按当前可见范围
|
组件使用不可变数据模型:替换 `data` 引用后会重新计算数据域和视口;大数据会按当前可见范围
|
||||||
和屏幕像素自动保峰降采样,而 tooltip、最近点查询和标注仍使用完整原始数据。
|
和屏幕像素自动保峰降采样,而 tooltip、最近点查询和标注仍使用完整原始数据。
|
||||||
|
|
||||||
@@ -16,18 +18,16 @@
|
|||||||
- 采样值、显式坐标点和多系列数据模型
|
- 采样值、显式坐标点和多系列数据模型
|
||||||
- `independent`、`separated`、`compact` 三种布局模式
|
- `independent`、`separated`、`compact` 三种布局模式
|
||||||
- 曲线、阶梯线、点符号和对称/非对称误差棒
|
- 曲线、阶梯线、点符号和对称/非对称误差棒
|
||||||
|
- 实线、虚线和点划线,可按系列独立配置
|
||||||
- 缩放过程事件、缩放结束按可视区间加载和视口重置
|
- 缩放过程事件、缩放结束按可视区间加载和视口重置
|
||||||
|
- 可选的空格拖拽平移,默认关闭并隔离多图表实例
|
||||||
- 多系列图例、受控显隐、网格分页和最多四根 Y 轴
|
- 多系列图例、受控显隐、网格分页和最多四根 Y 轴
|
||||||
|
- 按轨道控制水平/垂直网格线的显隐与颜色
|
||||||
- 受控标注、右键编辑、拖拽避让和自定义颜色
|
- 受控标注、右键编辑、拖拽避让和自定义颜色
|
||||||
- 标题、图框、坐标轴、时间单位和降采样参数可配置
|
- 标题、图框、坐标轴、零值参考线、净图和渲染参数可配置
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
|
||||||
pnpm dev
|
|
||||||
```
|
|
||||||
|
|
||||||
组件库将 Vue、D3、Ant Design Vue 和 vue3-colorpicker 作为 peer dependency;直接安装到业务项目时请一并
|
组件库将 Vue、D3、Ant Design Vue 和 vue3-colorpicker 作为 peer dependency;直接安装到业务项目时请一并
|
||||||
安装这些依赖:
|
安装这些依赖:
|
||||||
|
|
||||||
@@ -39,12 +39,14 @@ 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` |
|
||||||
|
| D3 | `>=7.9.0 <8` |
|
||||||
|
| vue3-colorpicker | `>=2.3.0 <3` |
|
||||||
|
|
||||||
安装时请确保业务项目中的 Vue 与 Ant Design Vue 版本满足上述范围。
|
安装时请确保业务项目中的 peer dependency 版本均满足上述范围。
|
||||||
|
|
||||||
## 发布
|
## 发布
|
||||||
|
|
||||||
@@ -96,24 +98,32 @@ const data = ref<WaveformData>({
|
|||||||
| `displayMode` | `'independent' \| 'separated' \| 'compact'` | `'independent'` | 图框布局 |
|
| `displayMode` | `'independent' \| 'separated' \| 'compact'` | `'independent'` | 图框布局 |
|
||||||
| `overlayMode` | `'single-axis' \| 'multi-axis'` | `'single-axis'` | 叠加曲线的 Y 轴模式 |
|
| `overlayMode` | `'single-axis' \| 'multi-axis'` | `'single-axis'` | 叠加曲线的 Y 轴模式 |
|
||||||
| `timeUnit` | `'s' \| 'ms'` | `'ms'` | 坐标轴和 tooltip 展示单位 |
|
| `timeUnit` | `'s' \| 'ms'` | `'ms'` | 坐标轴和 tooltip 展示单位 |
|
||||||
|
| `xLabel` / `yLabel` | `string` | `时间(timeUnit)` / `'幅值'` | 坐标轴名称 |
|
||||||
|
| `lineColor` | `string` | `'#0960bd'` | 单波形默认颜色 |
|
||||||
| `width` / `height` | `number` | 自适应 | 组件总尺寸,单位为 CSS 像素 |
|
| `width` / `height` | `number` | 自适应 | 组件总尺寸,单位为 CSS 像素 |
|
||||||
| `zoomable` / `showTooltip` | `boolean` | `true` / `true` | 缩放和数值 tooltip 开关 |
|
| `zoomable` / `showTooltip` | `boolean` | `true` / `true` | 缩放和数值 tooltip 开关 |
|
||||||
| `pannable` | `boolean` | `false` | 空格拖拽平移开关 |
|
| `pannable` | `boolean` | `false` | 空格拖拽平移开关 |
|
||||||
| `minZoomSpan` | `number` | 未设置 | 最小缩放跨度,使用原始 X 数据单位 |
|
| `minZoomSpan` | `number` | 未设置 | 最小缩放跨度,使用原始 X 数据单位 |
|
||||||
|
| `minVisiblePoints` | `number` | `0` | 缩放后至少保留的不同 X 坐标数 |
|
||||||
| `initialXDomain` | `[number, number]` | 未设置 | 所有图框的初始 X 范围 |
|
| `initialXDomain` | `[number, number]` | 未设置 | 所有图框的初始 X 范围 |
|
||||||
| `initialXDomains` | `Record<string, [number, number]>` | 未设置 | 按 track/series ID 配置初始范围 |
|
| `initialXDomains` | `Record<string, [number, number]>` | 未设置 | 按 track/series ID 配置初始范围 |
|
||||||
| `grid` | `WaveformGridOptions` | `{ rowCount: 2, columnCount: 1, showPagination: true }` | 网格和分页 |
|
| `grid` | `WaveformGridOptions` | `{ rowCount: 2, columnCount: 1, showPagination: true }` | 网格和分页 |
|
||||||
|
| `axes` | `WaveformAxesOptions` | 轴线均显示 | X/Y 轴基线显隐 |
|
||||||
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
||||||
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
||||||
|
| `frameNumber` | `string \| number` | 未设置 | 图框水印内容 |
|
||||||
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
||||||
| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 |
|
| `cleanView` | `boolean` | `false` | 仅保留波形的净图模式 |
|
||||||
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
||||||
|
| `annotationsVisible` | `boolean` | `true` | 标注图层显隐 |
|
||||||
|
| `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 |
|
||||||
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
||||||
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
||||||
|
|
||||||
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
||||||
`WaveformAnnotation`、`WaveformRenderingOptions`、`WaveformZeroLineOptions` 和
|
`WaveformAnnotation`、`WaveformLineStyle`、`WaveformRenderingOptions`、
|
||||||
`WaveformGridOptions`。
|
`WaveformAxesOptions`、`WaveformZeroLineOptions`、`WaveformGridOptions` 和
|
||||||
|
`WaveformGridTrackLines`。
|
||||||
|
|
||||||
### 数据结构
|
### 数据结构
|
||||||
|
|
||||||
@@ -157,7 +167,8 @@ const chartData: WaveformData = {
|
|||||||
|
|
||||||
```vue
|
```vue
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { WaveformChart } from './index'
|
import { WaveformChart } from 'waveform-analysis'
|
||||||
|
import 'waveform-analysis/style.css'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -311,28 +322,6 @@ const series = {
|
|||||||
`width` 和 `height` 始终表示组件总尺寸。标题显示后会从总高度中扣除标题区域,剩余高度
|
`width` 和 `height` 始终表示组件总尺寸。标题显示后会从总高度中扣除标题区域,剩余高度
|
||||||
用于 SVG 绘图区,因此启用标题不会扩大组件或破坏父容器布局。
|
用于 SVG 绘图区,因此启用标题不会扩大组件或破坏父容器布局。
|
||||||
|
|
||||||
宿主已有全局标题配置时,可以在未来接入组件库绘图链路时按以下方式映射:
|
|
||||||
|
|
||||||
```ts
|
|
||||||
const waveformTitle = {
|
|
||||||
visible: hasQueried && !cleanViewEnabled && titleStyle.enabled,
|
|
||||||
text: titleStyle.titleName.trim() || defaultTitleText,
|
|
||||||
align: titleStyle.align,
|
|
||||||
textStyle: {
|
|
||||||
color: titleStyle.color,
|
|
||||||
fontSize: titleStyle.fontSize,
|
|
||||||
fontFamily: titleStyle.fontFamily,
|
|
||||||
rotation: titleStyle.rotation,
|
|
||||||
fontWeight: titleStyle.bold ? 700 : 400,
|
|
||||||
fontStyle: titleStyle.italic ? 'italic' : 'normal',
|
|
||||||
textDecoration: titleStyle.underline ? 'underline' : 'none',
|
|
||||||
},
|
|
||||||
} satisfies WaveformTitleOptions
|
|
||||||
```
|
|
||||||
|
|
||||||
宿主的抽屉折叠状态不需要传给组件。替换绘图链路时应同步移除宿主外层标题,避免重复
|
|
||||||
渲染;当前宿主实现无需修改。
|
|
||||||
|
|
||||||
Demo 左侧控制面板提供标题实时预览,可配置标题名称、显隐、对齐、字体、字号、粗体、
|
Demo 左侧控制面板提供标题实时预览,可配置标题名称、显隐、对齐、字体、字号、粗体、
|
||||||
斜体、下划线、旋转和颜色。字号范围为 `8–72px`,旋转范围为 `-180–180°`;样式栏中的
|
斜体、下划线、旋转和颜色。字号范围为 `8–72px`,旋转范围为 `-180–180°`;样式栏中的
|
||||||
`A` 用于恢复常规字重、非斜体和无下划线,关闭标题不会清除已经填写的配置。
|
`A` 用于恢复常规字重、非斜体和无下划线,关闭标题不会清除已经填写的配置。
|
||||||
@@ -353,6 +342,8 @@ Demo 左侧控制面板提供标题实时预览,可配置标题名称、显隐
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`frameStyle.borderStyle` 支持 `solid`(实线)、`dashed`(虚线)和 `dotted`(点虚线)。
|
||||||
|
|
||||||
对应的公开类型为 `WaveformFrameStyle`。默认边框颜色为 `#1f2937`、线宽为 `1`、线型为
|
对应的公开类型为 `WaveformFrameStyle`。默认边框颜色为 `#1f2937`、线宽为 `1`、线型为
|
||||||
`solid`,背景透明。`borderWidth` 为 `0` 时隐藏边框;非有限值或负数会回退到默认线宽。
|
`solid`,背景透明。`borderWidth` 为 `0` 时隐藏边框;非有限值或负数会回退到默认线宽。
|
||||||
|
|
||||||
@@ -373,6 +364,10 @@ const hiddenSeriesIds = ref<string[]>([])
|
|||||||
v-model:hidden-series-ids="hiddenSeriesIds"
|
v-model:hidden-series-ids="hiddenSeriesIds"
|
||||||
:legend="{
|
:legend="{
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
|
trackPositions: {
|
||||||
|
'group-1': 'top-left',
|
||||||
|
'group-2': 'bottom',
|
||||||
|
},
|
||||||
orientation: 'auto',
|
orientation: 'auto',
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.45)',
|
backgroundColor: 'rgba(255, 255, 255, 0.45)',
|
||||||
interactive: true,
|
interactive: true,
|
||||||
@@ -380,6 +375,11 @@ const hiddenSeriesIds = ref<string[]>([])
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`legend.trackPositions` 按图框 ID 单独覆盖图例位置;同一个 `trackId` 组成的图框以该
|
||||||
|
`trackId` 为键,未设置 `trackId` 时以规范化后的 `series.id` 为键。未命中的图框继续使用
|
||||||
|
`legend.position`,两者都未配置时使用 `top-right`。当 `orientation` 为 `auto` 时,每个图框
|
||||||
|
会根据最终位置独立选择排列方向:`top`、`bottom` 为水平排列,其余位置为垂直排列。
|
||||||
|
|
||||||
未配置或传入空字符串时,图例背景默认使用 `rgba(255, 255, 255, 0.7)`。
|
未配置或传入空字符串时,图例背景默认使用 `rgba(255, 255, 255, 0.7)`。
|
||||||
`legend.interactive` 默认为 `false`;开启后可以单击或使用键盘操作图例项切换曲线显隐。
|
`legend.interactive` 默认为 `false`;开启后可以单击或使用键盘操作图例项切换曲线显隐。
|
||||||
调用方可通过 `hiddenSeriesIds` 和 `update:hidden-series-ids` 控制状态,也可使用
|
调用方可通过 `hiddenSeriesIds` 和 `update:hidden-series-ids` 控制状态,也可使用
|
||||||
@@ -445,6 +445,29 @@ tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失
|
|||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`axes` 可以分别隐藏 X/Y 轴的基线,同时保留刻度短线、刻度数字、科学计数倍率、单位和轴标题。
|
||||||
|
与关闭网格线、设置 `frameStyle` 组合后,可以只使用图框边框围住绘图区:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<WaveformChart
|
||||||
|
:data="chartData"
|
||||||
|
:axes="{
|
||||||
|
x: { lineVisible: false },
|
||||||
|
y: { lineVisible: false },
|
||||||
|
}"
|
||||||
|
:grid="{
|
||||||
|
trackLines: {
|
||||||
|
voltage: { horizontal: false, vertical: false },
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
:frame-style="{
|
||||||
|
borderColor: '#1f2937',
|
||||||
|
borderWidth: 1,
|
||||||
|
borderStyle: 'solid',
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
`interactionMode` 可选 `zoom` 或 `annotation`,默认使用缩放模式。右键绘图区可直接打开
|
`interactionMode` 可选 `zoom` 或 `annotation`,默认使用缩放模式。右键绘图区可直接打开
|
||||||
标注编辑器,无需切换交互模式。`zoomable`、`pannable` 和 `showTooltip` 可分别控制缩放、
|
标注编辑器,无需切换交互模式。`zoomable`、`pannable` 和 `showTooltip` 可分别控制缩放、
|
||||||
空格拖拽平移和 tooltip;平移默认关闭。
|
空格拖拽平移和 tooltip;平移默认关闭。
|
||||||
@@ -455,8 +478,8 @@ tooltip 仍然可用,切换回普通模式后原有配置和标注不会丢失
|
|||||||
组件按不可变数据处理:替换 `data` 引用会重新过滤、排序和缓存坐标域,并重置视口;
|
组件按不可变数据处理:替换 `data` 引用会重新过滤、排序和缓存坐标域,并重置视口;
|
||||||
原地修改已有数组不会触发缓存刷新。建议通过 `shallowRef` 保存大数据并整体替换引用。
|
原地修改已有数组不会触发缓存刷新。建议通过 `shallowRef` 保存大数据并整体替换引用。
|
||||||
规范化始终保留所有有效点,坐标域、误差棒、tooltip 和标注均使用完整数据;绘制路径会根据
|
规范化始终保留所有有效点,坐标域、误差棒、tooltip 和标注均使用完整数据;绘制路径会根据
|
||||||
当前视口和 `rendering` 配置自动降采样。如需在传入组件前主动压缩数据,可使用公开的
|
当前视口和 `rendering` 配置自动降采样。调用方如需在传入组件前主动压缩数据,应自行保留
|
||||||
`downsampleLTTB`、`downsampleMinMax` 或 `adaptiveSampling` 工具。
|
原始数据,以免影响 tooltip、标注和误差范围的精度。
|
||||||
|
|
||||||
默认在可见点超过 2,000 时进行降采样,每个像素最多渲染 4 个保峰点。可按业务调整:
|
默认在可见点超过 2,000 时进行降采样,每个像素最多渲染 4 个保峰点。可按业务调整:
|
||||||
|
|
||||||
@@ -496,7 +519,8 @@ import {
|
|||||||
WaveformChart,
|
WaveformChart,
|
||||||
type WaveformAnnotation,
|
type WaveformAnnotation,
|
||||||
type WaveformInteractionMode,
|
type WaveformInteractionMode,
|
||||||
} from './index'
|
} from 'waveform-analysis'
|
||||||
|
import 'waveform-analysis/style.css'
|
||||||
|
|
||||||
const annotations = ref<WaveformAnnotation[]>([])
|
const annotations = ref<WaveformAnnotation[]>([])
|
||||||
const annotationsVisible = ref(true)
|
const annotationsVisible = ref(true)
|
||||||
@@ -542,7 +566,7 @@ X、Y 轴会根据各自完整显示域选择格式:最大绝对值在 `[0.01,
|
|||||||
| --------------------------------------------------------------- | -------------------------------------------------------------- |
|
| --------------------------------------------------------------- | -------------------------------------------------------------- |
|
||||||
| `point-hover` | 当前最近点变化时触发,离开图表时传入 `null` |
|
| `point-hover` | 当前最近点变化时触发,离开图表时传入 `null` |
|
||||||
| `zoom-change` | 缩放过程中触发,参数为 `[start, end]` |
|
| `zoom-change` | 缩放过程中触发,参数为 `[start, end]` |
|
||||||
| `zoom-end` | 滚轮放大结束后触发;独立分图模式附带 `trackIndex`、`seriesIds` |
|
| `zoom-end` | 滚轮或框选结束后触发;`gesture` 区分二者,独立模式附带轨道信息 |
|
||||||
| `zoom-reset` | 双击重置视口时触发,调用方应恢复首次完整数据 |
|
| `zoom-reset` | 双击重置视口时触发,调用方应恢复首次完整数据 |
|
||||||
| `page-change` | 分页变化,参数为当前页和总页数 |
|
| `page-change` | 分页变化,参数为当前页和总页数 |
|
||||||
| `series-visibility-change` | 图例切换曲线显隐时触发 |
|
| `series-visibility-change` | 图例切换曲线显隐时触发 |
|
||||||
@@ -585,7 +609,7 @@ pnpm build
|
|||||||
发布由推送版本 tag 触发。先将 `package.json` 的 `version` 更新为目标版本并提交,再创建同版本 tag:
|
发布由推送版本 tag 触发。先将 `package.json` 的 `version` 更新为目标版本并提交,再创建同版本 tag:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git tag -a v0.1.7 -m "Release v0.1.7"
|
git tag -a v0.1.15 -m "Release v0.1.15"
|
||||||
git push origin main --follow-tags
|
git push origin main --follow-tags
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "waveform-analysis",
|
"name": "waveform-analysis",
|
||||||
"version": "0.1.15",
|
"version": "0.1.17",
|
||||||
"main": "./dist/index.cjs",
|
"main": "./dist/index.cjs",
|
||||||
"module": "./dist/index.js",
|
"module": "./dist/index.js",
|
||||||
"types": "./dist/types/index.d.ts",
|
"types": "./dist/types/index.d.ts",
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
expect(gridSizeInputs[1]?.props('value')).toBe(1)
|
expect(gridSizeInputs[1]?.props('value')).toBe(1)
|
||||||
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)
|
||||||
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)
|
||||||
@@ -62,6 +64,9 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
expect(frameControls.text()).toContain('背景颜色')
|
expect(frameControls.text()).toContain('背景颜色')
|
||||||
expect(frameControls.find('[aria-label="图框线宽"]').exists()).toBe(true)
|
expect(frameControls.find('[aria-label="图框线宽"]').exists()).toBe(true)
|
||||||
expect(frameControls.find('[aria-label="图框线型"]').exists()).toBe(true)
|
expect(frameControls.find('[aria-label="图框线型"]').exists()).toBe(true)
|
||||||
|
expect(
|
||||||
|
frameControls.get('[aria-label="图框线型"]').getComponent(Select).props('options'),
|
||||||
|
).toContainEqual({ label: '点虚线', value: 'dotted' })
|
||||||
expect(frameControls.find('[aria-label="显示图框水印"]').exists()).toBe(true)
|
expect(frameControls.find('[aria-label="显示图框水印"]').exists()).toBe(true)
|
||||||
expect(frameControls.get('.frame-style-control--switch .ant-switch').classes()).toContain(
|
expect(frameControls.get('.frame-style-control--switch .ant-switch').classes()).toContain(
|
||||||
'ant-switch-small',
|
'ant-switch-small',
|
||||||
@@ -129,6 +134,27 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('passes independent X and Y axis-line controls to the chart', async () => {
|
||||||
|
const wrapper = mount(App)
|
||||||
|
await flushPromises()
|
||||||
|
const chart = wrapper.getComponent(WaveformChart)
|
||||||
|
|
||||||
|
expect(chart.props('axes')).toEqual({
|
||||||
|
x: { lineVisible: false },
|
||||||
|
y: { lineVisible: false },
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.get('[aria-label="显示横轴线"]').trigger('click')
|
||||||
|
await wrapper.get('[aria-label="显示纵轴线"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(chart.props('axes')).toEqual({
|
||||||
|
x: { lineVisible: true },
|
||||||
|
y: { lineVisible: true },
|
||||||
|
})
|
||||||
|
wrapper.unmount()
|
||||||
|
})
|
||||||
|
|
||||||
it('passes tooltip and per-series line-style controls to the chart', async () => {
|
it('passes tooltip and per-series line-style controls to the chart', async () => {
|
||||||
const wrapper = mount(App)
|
const wrapper = mount(App)
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|||||||
21
src/App.vue
21
src/App.vue
@@ -7,6 +7,7 @@ import 'vue3-colorpicker/style.css'
|
|||||||
import {
|
import {
|
||||||
WaveformChart,
|
WaveformChart,
|
||||||
type WaveformAnnotation,
|
type WaveformAnnotation,
|
||||||
|
type WaveformAxesOptions,
|
||||||
type WaveformData,
|
type WaveformData,
|
||||||
type WaveformDisplayMode,
|
type WaveformDisplayMode,
|
||||||
type WaveformFrameStyle,
|
type WaveformFrameStyle,
|
||||||
@@ -60,13 +61,15 @@ const rowCount = ref(4)
|
|||||||
const columnCount = ref(1)
|
const columnCount = ref(1)
|
||||||
const frameBorderColor = ref('#1f2937')
|
const frameBorderColor = ref('#1f2937')
|
||||||
const frameBorderWidth = ref(1)
|
const frameBorderWidth = ref(1)
|
||||||
const frameBorderStyle = ref<'solid' | 'dashed'>('solid')
|
const frameBorderStyle = ref<NonNullable<WaveformFrameStyle['borderStyle']>>('solid')
|
||||||
const frameBackgroundColor = ref('rgba(255, 255, 255, 0)')
|
const frameBackgroundColor = ref('rgba(255, 255, 255, 0)')
|
||||||
const frameWatermarkVisible = ref(true)
|
const frameWatermarkVisible = ref(true)
|
||||||
const horizontalGridVisible = ref(true)
|
const horizontalGridVisible = ref(true)
|
||||||
const horizontalGridColor = ref('#dfe5ef')
|
const horizontalGridColor = ref('#dfe5ef')
|
||||||
const verticalGridVisible = ref(true)
|
const verticalGridVisible = ref(true)
|
||||||
const verticalGridColor = ref('#dfe5ef')
|
const verticalGridColor = ref('#dfe5ef')
|
||||||
|
const xAxisLineVisible = ref(false)
|
||||||
|
const yAxisLineVisible = ref(false)
|
||||||
const annotations = ref<WaveformAnnotation[]>([])
|
const annotations = ref<WaveformAnnotation[]>([])
|
||||||
const annotationsVisible = ref(true)
|
const annotationsVisible = ref(true)
|
||||||
const cleanView = ref(false)
|
const cleanView = ref(false)
|
||||||
@@ -109,6 +112,7 @@ const legendOrientationOptions: Array<{ label: string; value: WaveformLegendOrie
|
|||||||
const frameBorderStyleOptions = [
|
const frameBorderStyleOptions = [
|
||||||
{ label: '实线', value: 'solid' },
|
{ label: '实线', value: 'solid' },
|
||||||
{ label: '虚线', value: 'dashed' },
|
{ label: '虚线', value: 'dashed' },
|
||||||
|
{ label: '点虚线', value: 'dotted' },
|
||||||
]
|
]
|
||||||
const zeroLineDashOptions = [
|
const zeroLineDashOptions = [
|
||||||
{ label: '虚线', value: '6 4' },
|
{ label: '虚线', value: '6 4' },
|
||||||
@@ -136,6 +140,10 @@ const frameStyle = computed<WaveformFrameStyle>(() => ({
|
|||||||
borderStyle: frameBorderStyle.value,
|
borderStyle: frameBorderStyle.value,
|
||||||
backgroundColor: frameBackgroundColor.value,
|
backgroundColor: frameBackgroundColor.value,
|
||||||
}))
|
}))
|
||||||
|
const axes = computed<WaveformAxesOptions>(() => ({
|
||||||
|
x: { lineVisible: xAxisLineVisible.value },
|
||||||
|
y: { lineVisible: yAxisLineVisible.value },
|
||||||
|
}))
|
||||||
const zeroLine = computed<WaveformZeroLineOptions>(() => ({
|
const zeroLine = computed<WaveformZeroLineOptions>(() => ({
|
||||||
visible: zeroLineVisible.value,
|
visible: zeroLineVisible.value,
|
||||||
color: zeroLineColor.value,
|
color: zeroLineColor.value,
|
||||||
@@ -510,7 +518,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="control-section">
|
<section class="control-section">
|
||||||
<h2>网格线</h2>
|
<h2>网格与轴线</h2>
|
||||||
<div class="grid-line-controls">
|
<div class="grid-line-controls">
|
||||||
<div class="grid-line-control">
|
<div class="grid-line-control">
|
||||||
<span>水平网格</span>
|
<span>水平网格</span>
|
||||||
@@ -548,6 +556,14 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
|
|||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="grid-line-control grid-line-control--switch-only">
|
||||||
|
<span>横轴线</span>
|
||||||
|
<Switch v-model:checked="xAxisLineVisible" size="small" aria-label="显示横轴线" />
|
||||||
|
</div>
|
||||||
|
<div class="grid-line-control grid-line-control--switch-only">
|
||||||
|
<span>纵轴线</span>
|
||||||
|
<Switch v-model:checked="yAxisLineVisible" size="small" aria-label="显示纵轴线" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -773,6 +789,7 @@ onBeforeUnmount(() => window.removeEventListener('keydown', handleWindowKeydown)
|
|||||||
interactive: true,
|
interactive: true,
|
||||||
}"
|
}"
|
||||||
:frame-style="frameStyle"
|
:frame-style="frameStyle"
|
||||||
|
:axes="axes"
|
||||||
:clean-view="cleanView"
|
:clean-view="cleanView"
|
||||||
:show-tooltip="showTooltip"
|
:show-tooltip="showTooltip"
|
||||||
:zero-line="zeroLine"
|
:zero-line="zeroLine"
|
||||||
|
|||||||
@@ -349,6 +349,75 @@ describe('WaveformChart', () => {
|
|||||||
expect(zeroLines[0].attributes('y1')).not.toBe(zeroLines[1].attributes('y1'))
|
expect(zeroLines[0].attributes('y1')).not.toBe(zeroLines[1].attributes('y1'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('hides X and Y axis lines independently while preserving axis text and the frame', async () => {
|
||||||
|
const wrapper = await mountSizedChart({
|
||||||
|
kind: 'series',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: 'channel',
|
||||||
|
name: 'Voltage',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: -1 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const xAxis = wrapper.get('.waveform-chart__axis--x')
|
||||||
|
const yAxis = wrapper.get('.waveform-chart__axis--y')
|
||||||
|
|
||||||
|
expect(xAxis.classes()).not.toContain('waveform-track__axis--line-hidden')
|
||||||
|
expect(yAxis.classes()).not.toContain('waveform-track__axis--line-hidden')
|
||||||
|
|
||||||
|
await wrapper.setProps({ axes: { x: { lineVisible: false } } })
|
||||||
|
await flushPromises()
|
||||||
|
expect(xAxis.classes()).toContain('waveform-track__axis--line-hidden')
|
||||||
|
expect(yAxis.classes()).not.toContain('waveform-track__axis--line-hidden')
|
||||||
|
expect(xAxis.get('path.domain').attributes('display')).toBe('none')
|
||||||
|
expect(
|
||||||
|
xAxis.findAll('.tick line').every((line) => line.attributes('display') === undefined),
|
||||||
|
).toBe(true)
|
||||||
|
expect(yAxis.get('path.domain').attributes('display')).toBeUndefined()
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
axes: {
|
||||||
|
x: { lineVisible: false },
|
||||||
|
y: { lineVisible: false },
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
rowCount: 1,
|
||||||
|
columnCount: 1,
|
||||||
|
trackLines: { channel: { horizontal: false, vertical: false } },
|
||||||
|
},
|
||||||
|
frameStyle: { borderColor: '#dc2626', borderWidth: 2 },
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(xAxis.classes()).toContain('waveform-track__axis--line-hidden')
|
||||||
|
expect(yAxis.classes()).toContain('waveform-track__axis--line-hidden')
|
||||||
|
expect(yAxis.get('path.domain').attributes('display')).toBe('none')
|
||||||
|
expect(
|
||||||
|
yAxis.findAll('.tick line').every((line) => line.attributes('display') === undefined),
|
||||||
|
).toBe(true)
|
||||||
|
expect(xAxis.findAll('text').length).toBeGreaterThan(0)
|
||||||
|
expect(yAxis.findAll('text').length).toBeGreaterThan(0)
|
||||||
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
|
||||||
|
expect(wrapper.get('.waveform-chart__y-axis-label').text()).toBe('Voltage')
|
||||||
|
expect(wrapper.findAll('[data-grid-direction]')).toHaveLength(0)
|
||||||
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
||||||
|
stroke: '#dc2626',
|
||||||
|
'stroke-width': '2',
|
||||||
|
})
|
||||||
|
|
||||||
|
await wrapper.setProps({ axes: { x: { lineVisible: true }, y: { lineVisible: true } } })
|
||||||
|
await flushPromises()
|
||||||
|
expect(xAxis.get('path.domain').attributes('display')).toBeUndefined()
|
||||||
|
expect(yAxis.get('path.domain').attributes('display')).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it('preserves a titled multi-axis plot and hides auxiliary layers in clean view', async () => {
|
it('preserves a titled multi-axis plot and hides auxiliary layers in clean view', async () => {
|
||||||
const data: WaveformData = {
|
const data: WaveformData = {
|
||||||
kind: 'series',
|
kind: 'series',
|
||||||
@@ -1079,6 +1148,61 @@ describe('WaveformChart', () => {
|
|||||||
expect(wrapper.attributes('data-chart-left-margin')).toBe('64')
|
expect(wrapper.attributes('data-chart-left-margin')).toBe('64')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('resolves legend positions by stable track id across pages', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'series',
|
||||||
|
series: Array.from({ length: 8 }, (_, index) => ({
|
||||||
|
id: `series-${index}`,
|
||||||
|
trackId: `frame-${Math.floor(index / 2)}`,
|
||||||
|
name: `series ${index}`,
|
||||||
|
data: {
|
||||||
|
kind: 'points' as const,
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: index },
|
||||||
|
{ x: 1, y: index + 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
grid: { rowCount: 2, columnCount: 1, showPagination: true },
|
||||||
|
legend: {
|
||||||
|
position: 'left',
|
||||||
|
orientation: 'auto',
|
||||||
|
trackPositions: {
|
||||||
|
'frame-0': 'top',
|
||||||
|
'frame-1': 'bottom-right',
|
||||||
|
'frame-2': 'bottom',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const legendForTrack = (trackId: string) =>
|
||||||
|
wrapper.get(`[data-legend-track-id="${trackId}"] .waveform-chart__legend`)
|
||||||
|
|
||||||
|
expect(legendForTrack('frame-0').attributes('data-position')).toBe('top')
|
||||||
|
expect(legendForTrack('frame-0').attributes('data-orientation')).toBe('horizontal')
|
||||||
|
expect(legendForTrack('frame-1').attributes('data-position')).toBe('bottom-right')
|
||||||
|
expect(legendForTrack('frame-1').attributes('data-orientation')).toBe('vertical')
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
legend: {
|
||||||
|
position: 'left',
|
||||||
|
orientation: 'vertical',
|
||||||
|
trackPositions: { 'frame-0': 'top', 'frame-1': 'bottom-right', 'frame-2': 'bottom' },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
expect(legendForTrack('frame-0').attributes('data-orientation')).toBe('vertical')
|
||||||
|
|
||||||
|
await wrapper.get('.ant-pagination-next button').trigger('click')
|
||||||
|
expect(legendForTrack('frame-2').attributes('data-position')).toBe('bottom')
|
||||||
|
expect(legendForTrack('frame-2').attributes('data-orientation')).toBe('vertical')
|
||||||
|
expect(legendForTrack('frame-3').attributes('data-position')).toBe('left')
|
||||||
|
expect(legendForTrack('frame-3').attributes('data-orientation')).toBe('vertical')
|
||||||
|
})
|
||||||
|
|
||||||
it('applies a configurable alpha background to every visible legend', async () => {
|
it('applies a configurable alpha background to every visible legend', async () => {
|
||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
@@ -2329,6 +2453,17 @@ describe('WaveformChart', () => {
|
|||||||
expect(wrapper.get('.waveform-chart__plot-frame').attributes('stroke-width')).toBe('1')
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes('stroke-width')).toBe('1')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('renders a dotted frame with rounded dots', async () => {
|
||||||
|
const wrapper = await mountSizedChart(gridSeries(1), {
|
||||||
|
frameStyle: { borderStyle: 'dotted' },
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
||||||
|
'stroke-dasharray': '1 3',
|
||||||
|
'stroke-linecap': 'round',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('continues minor x-grid lines beyond the final major tick to the exact endpoint', async () => {
|
it('continues minor x-grid lines beyond the final major tick to the exact endpoint', async () => {
|
||||||
const wrapper = await mountSizedChart({
|
const wrapper = await mountSizedChart({
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
type WaveformAnnotation,
|
type WaveformAnnotation,
|
||||||
|
type WaveformAxesOptions,
|
||||||
type WaveformData,
|
type WaveformData,
|
||||||
type WaveformDisplayMode,
|
type WaveformDisplayMode,
|
||||||
type WaveformFrameStyle,
|
type WaveformFrameStyle,
|
||||||
@@ -121,6 +122,7 @@ const props = withDefaults(
|
|||||||
timeUnit?: 's' | 'ms'
|
timeUnit?: 's' | 'ms'
|
||||||
frameNumber?: string | number
|
frameNumber?: string | number
|
||||||
frameStyle?: WaveformFrameStyle
|
frameStyle?: WaveformFrameStyle
|
||||||
|
axes?: WaveformAxesOptions
|
||||||
annotations?: WaveformAnnotation[]
|
annotations?: WaveformAnnotation[]
|
||||||
annotationsVisible?: boolean
|
annotationsVisible?: boolean
|
||||||
interactionMode?: WaveformInteractionMode
|
interactionMode?: WaveformInteractionMode
|
||||||
@@ -251,7 +253,9 @@ function handleInteractionKeyDown(event: KeyboardEvent) {
|
|||||||
const target = event.target
|
const target = event.target
|
||||||
if (
|
if (
|
||||||
target instanceof Element &&
|
target instanceof Element &&
|
||||||
target.closest('button, input, select, textarea, [contenteditable]:not([contenteditable="false"])')
|
target.closest(
|
||||||
|
'button, input, select, textarea, [contenteditable]:not([contenteditable="false"])',
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -290,14 +294,16 @@ const containerStyle = computed(() => ({
|
|||||||
width: fixedWidth.value === undefined ? '100%' : `${fixedWidth.value}px`,
|
width: fixedWidth.value === undefined ? '100%' : `${fixedWidth.value}px`,
|
||||||
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 isCleanView = computed(() => props.cleanView === true)
|
const isCleanView = computed(() => props.cleanView === true)
|
||||||
const resolvedZeroLine = computed(() => {
|
const resolvedZeroLine = computed(() => {
|
||||||
const width = props.zeroLine?.width
|
const width = props.zeroLine?.width
|
||||||
return {
|
return {
|
||||||
visible: props.zeroLine?.visible === true,
|
visible: props.zeroLine?.visible === true,
|
||||||
color: props.zeroLine?.color || ZERO_LINE_DEFAULTS.COLOR,
|
color: props.zeroLine?.color || ZERO_LINE_DEFAULTS.COLOR,
|
||||||
width: typeof width === 'number' && Number.isFinite(width) && width > 0 ? width : ZERO_LINE_DEFAULTS.WIDTH,
|
width:
|
||||||
|
typeof width === 'number' && Number.isFinite(width) && width > 0
|
||||||
|
? width
|
||||||
|
: ZERO_LINE_DEFAULTS.WIDTH,
|
||||||
dash: props.zeroLine?.dash ?? ZERO_LINE_DEFAULTS.DASH,
|
dash: props.zeroLine?.dash ?? ZERO_LINE_DEFAULTS.DASH,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -311,13 +317,17 @@ const hiddenSeriesIdSet = computed(() =>
|
|||||||
: new Set(props.hiddenSeriesIds),
|
: new Set(props.hiddenSeriesIds),
|
||||||
)
|
)
|
||||||
const resolvedHiddenSeriesIds = computed(() => Array.from(hiddenSeriesIdSet.value))
|
const resolvedHiddenSeriesIds = computed(() => Array.from(hiddenSeriesIdSet.value))
|
||||||
const legendOrientation = computed<Exclude<WaveformLegendOrientation, 'auto'>>(() => {
|
function resolveLegendPosition(trackId: string): WaveformLegendPosition {
|
||||||
|
return props.legend?.trackPositions?.[trackId] ?? props.legend?.position ?? 'top-right'
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLegendOrientation(
|
||||||
|
position: WaveformLegendPosition,
|
||||||
|
): Exclude<WaveformLegendOrientation, 'auto'> {
|
||||||
const orientation = props.legend?.orientation ?? 'auto'
|
const orientation = props.legend?.orientation ?? 'auto'
|
||||||
if (orientation !== 'auto') return orientation
|
if (orientation !== 'auto') return orientation
|
||||||
return legendPosition.value === 'top' || legendPosition.value === 'bottom'
|
return position === 'top' || position === 'bottom' ? 'horizontal' : 'vertical'
|
||||||
? 'horizontal'
|
}
|
||||||
: 'vertical'
|
|
||||||
})
|
|
||||||
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
const resolvedTitleText = computed(() => props.title?.text.trim() ?? '')
|
||||||
const titleAreaReserved = computed(
|
const titleAreaReserved = computed(
|
||||||
() =>
|
() =>
|
||||||
@@ -326,7 +336,9 @@ const titleAreaReserved = computed(
|
|||||||
const titleVisible = computed(() => titleAreaReserved.value && !isCleanView.value)
|
const titleVisible = computed(() => titleAreaReserved.value && !isCleanView.value)
|
||||||
const titleFontSize = computed(() => {
|
const titleFontSize = computed(() => {
|
||||||
const fontSize = props.title?.textStyle?.fontSize
|
const fontSize = props.title?.textStyle?.fontSize
|
||||||
return Number.isFinite(fontSize) && (fontSize ?? 0) > 0 ? (fontSize as number) : TITLE_DEFAULT_FONT_SIZE
|
return Number.isFinite(fontSize) && (fontSize ?? 0) > 0
|
||||||
|
? (fontSize as number)
|
||||||
|
: TITLE_DEFAULT_FONT_SIZE
|
||||||
})
|
})
|
||||||
const titleRotation = computed(() => {
|
const titleRotation = computed(() => {
|
||||||
const rotation = props.title?.textStyle?.rotation
|
const rotation = props.title?.textStyle?.rotation
|
||||||
@@ -351,7 +363,10 @@ const estimatedTitleWidth = computed(() => {
|
|||||||
const spacingWidth = Number.isFinite(letterSpacing)
|
const spacingWidth = Number.isFinite(letterSpacing)
|
||||||
? Math.max(0, resolvedTitleText.value.length - 1) * letterSpacing
|
? Math.max(0, resolvedTitleText.value.length - 1) * letterSpacing
|
||||||
: 0
|
: 0
|
||||||
return Math.max(1, resolvedTitleText.value.length * titleFontSize.value * TITLE_CHAR_WIDTH_RATIO + spacingWidth)
|
return Math.max(
|
||||||
|
1,
|
||||||
|
resolvedTitleText.value.length * titleFontSize.value * TITLE_CHAR_WIDTH_RATIO + spacingWidth,
|
||||||
|
)
|
||||||
})
|
})
|
||||||
const titleAvailableWidth = computed(() => {
|
const titleAvailableWidth = computed(() => {
|
||||||
const measuredAvailableWidth = chartWidth.value - TITLE_AREA_HORIZONTAL_PADDING * 2
|
const measuredAvailableWidth = chartWidth.value - TITLE_AREA_HORIZONTAL_PADDING * 2
|
||||||
@@ -863,10 +878,14 @@ function clearZoomBindings() {
|
|||||||
|
|
||||||
function resolveMaximumZoomScale(domain: [number, number]): number {
|
function resolveMaximumZoomScale(domain: [number, number]): number {
|
||||||
const minZoomSpan = props.minZoomSpan
|
const minZoomSpan = props.minZoomSpan
|
||||||
if (!Number.isFinite(minZoomSpan) || (minZoomSpan ?? 0) <= 0) return ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE
|
if (!Number.isFinite(minZoomSpan) || (minZoomSpan ?? 0) <= 0)
|
||||||
|
return ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE
|
||||||
const domainSpan = Math.abs(domain[1] - domain[0])
|
const domainSpan = Math.abs(domain[1] - domain[0])
|
||||||
if (!Number.isFinite(domainSpan) || domainSpan <= 0) return ZOOM_CONSTRAINTS.MIN_SCALE
|
if (!Number.isFinite(domainSpan) || domainSpan <= 0) return ZOOM_CONSTRAINTS.MIN_SCALE
|
||||||
return Math.min(ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE, Math.max(ZOOM_CONSTRAINTS.MIN_SCALE, domainSpan / (minZoomSpan ?? domainSpan)))
|
return Math.min(
|
||||||
|
ZOOM_CONSTRAINTS.DEFAULT_MAX_SCALE,
|
||||||
|
Math.max(ZOOM_CONSTRAINTS.MIN_SCALE, domainSpan / (minZoomSpan ?? domainSpan)),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function canZoomTrack(track: TrackLayout): boolean {
|
function canZoomTrack(track: TrackLayout): boolean {
|
||||||
@@ -1926,6 +1945,7 @@ onBeforeUnmount(() => {
|
|||||||
:interaction-mode="activeInteractionMode"
|
:interaction-mode="activeInteractionMode"
|
||||||
:frame-number="resolveFrameNumber(track.index)"
|
:frame-number="resolveFrameNumber(track.index)"
|
||||||
:frame-style="frameStyle"
|
:frame-style="frameStyle"
|
||||||
|
:axes="axes"
|
||||||
:clean-view="isCleanView"
|
:clean-view="isCleanView"
|
||||||
:zero-line="resolvedZeroLine"
|
:zero-line="resolvedZeroLine"
|
||||||
:time-unit="timeUnit"
|
:time-unit="timeUnit"
|
||||||
@@ -1966,13 +1986,14 @@ onBeforeUnmount(() => {
|
|||||||
:key="`legend-${track.index}-${track.series.name}`"
|
:key="`legend-${track.index}-${track.series.name}`"
|
||||||
class="waveform-chart__legend-track"
|
class="waveform-chart__legend-track"
|
||||||
:data-legend-track-index="track.index"
|
:data-legend-track-index="track.index"
|
||||||
|
:data-legend-track-id="track.id"
|
||||||
:transform="`translate(${track.left}, ${track.top})`"
|
:transform="`translate(${track.left}, ${track.top})`"
|
||||||
>
|
>
|
||||||
<WaveformLegend
|
<WaveformLegend
|
||||||
v-if="!track.isEmpty && track.legendSeries.length > 1"
|
v-if="!track.isEmpty && track.legendSeries.length > 1"
|
||||||
:series="track.legendSeries"
|
:series="track.legendSeries"
|
||||||
:position="legendPosition"
|
:position="resolveLegendPosition(track.id)"
|
||||||
:orientation="legendOrientation"
|
:orientation="resolveLegendOrientation(resolveLegendPosition(track.id))"
|
||||||
:background-color="legendBackgroundColor"
|
:background-color="legendBackgroundColor"
|
||||||
:interactive="legendInteractive"
|
:interactive="legendInteractive"
|
||||||
:hidden-series-ids="resolvedHiddenSeriesIds"
|
:hidden-series-ids="resolvedHiddenSeriesIds"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
id: displayTrack.id,
|
||||||
index,
|
index,
|
||||||
series,
|
series,
|
||||||
seriesList: displayTrack.visibleSeries,
|
seriesList: displayTrack.visibleSeries,
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ export interface HoveredSeriesPoint extends DisplaySeries {
|
|||||||
* 轨道布局
|
* 轨道布局
|
||||||
*/
|
*/
|
||||||
export interface TrackLayout {
|
export interface TrackLayout {
|
||||||
|
/** Stable track key derived from trackId, or from the series id when trackId is omitted. */
|
||||||
|
id: string
|
||||||
index: number
|
index: number
|
||||||
series: DisplaySeries
|
series: DisplaySeries
|
||||||
/** Visible series used by rendering and interaction code. */
|
/** Visible series used by rendering and interaction code. */
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformAxesOptions,
|
||||||
WaveformZeroLineOptions,
|
WaveformZeroLineOptions,
|
||||||
SingleWaveformData,
|
SingleWaveformData,
|
||||||
WaveformLineType,
|
WaveformLineType,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformAxesOptions,
|
||||||
WaveformZeroLineOptions,
|
WaveformZeroLineOptions,
|
||||||
WaveformPoint,
|
WaveformPoint,
|
||||||
WaveformSeries,
|
WaveformSeries,
|
||||||
|
|||||||
@@ -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, WaveformZeroLineOptions } from '../../types'
|
import type { WaveformAxesOptions, WaveformFrameStyle, WaveformZeroLineOptions } from '../../types'
|
||||||
import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types'
|
import type { WaveformDisplayMode, WaveformInteractionMode } from '../data/types'
|
||||||
import type {
|
import type {
|
||||||
DisplaySeries,
|
DisplaySeries,
|
||||||
@@ -31,6 +31,8 @@ interface Props {
|
|||||||
frameNumber?: string | number
|
frameNumber?: string | number
|
||||||
/** 图框样式 */
|
/** 图框样式 */
|
||||||
frameStyle?: WaveformFrameStyle
|
frameStyle?: WaveformFrameStyle
|
||||||
|
/** 坐标轴线显示选项 */
|
||||||
|
axes?: WaveformAxesOptions
|
||||||
/** 时间单位 */
|
/** 时间单位 */
|
||||||
timeUnit: 's' | 'ms'
|
timeUnit: 's' | 'ms'
|
||||||
/** 悬浮点(用于显示十字线) */
|
/** 悬浮点(用于显示十字线) */
|
||||||
@@ -72,7 +74,10 @@ const resolvedFrameStyle = computed(() => {
|
|||||||
typeof borderWidth === 'number' && Number.isFinite(borderWidth) && borderWidth >= 0
|
typeof borderWidth === 'number' && Number.isFinite(borderWidth) && borderWidth >= 0
|
||||||
? borderWidth
|
? borderWidth
|
||||||
: 1,
|
: 1,
|
||||||
borderStyle: props.frameStyle?.borderStyle === 'dashed' ? 'dashed' : 'solid',
|
borderStyle:
|
||||||
|
props.frameStyle?.borderStyle === 'dashed' || props.frameStyle?.borderStyle === 'dotted'
|
||||||
|
? props.frameStyle.borderStyle
|
||||||
|
: 'solid',
|
||||||
backgroundColor: props.frameStyle?.backgroundColor || 'transparent',
|
backgroundColor: props.frameStyle?.backgroundColor || 'transparent',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -138,11 +143,16 @@ function renderAxes() {
|
|||||||
|
|
||||||
yAxis.tickValues(axis.tickValues)
|
yAxis.tickValues(axis.tickValues)
|
||||||
|
|
||||||
select(element).call(yAxis)
|
const selection = select(element)
|
||||||
|
selection.call(yAxis)
|
||||||
|
selection
|
||||||
|
.selectAll('path.domain')
|
||||||
|
.attr('display', props.axes?.y?.lineVisible === false ? 'none' : null)
|
||||||
})
|
})
|
||||||
|
|
||||||
if (xAxisElement.value) {
|
if (xAxisElement.value) {
|
||||||
select(xAxisElement.value).call(
|
const selection = select(xAxisElement.value)
|
||||||
|
selection.call(
|
||||||
axisBottom(props.track.xScale)
|
axisBottom(props.track.xScale)
|
||||||
.tickValues(props.track.xAxisTickValues)
|
.tickValues(props.track.xAxisTickValues)
|
||||||
.tickFormat((value) =>
|
.tickFormat((value) =>
|
||||||
@@ -156,6 +166,9 @@ function renderAxes() {
|
|||||||
.tickPadding(7)
|
.tickPadding(7)
|
||||||
.tickSizeOuter(0),
|
.tickSizeOuter(0),
|
||||||
)
|
)
|
||||||
|
selection
|
||||||
|
.selectAll('path.domain')
|
||||||
|
.attr('display', props.axes?.x?.lineVisible === false ? 'none' : null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +184,8 @@ watch(
|
|||||||
() => props.track.xAxisTickValues,
|
() => props.track.xAxisTickValues,
|
||||||
() => props.track.yAxisTickValues,
|
() => props.track.yAxisTickValues,
|
||||||
() => props.timeUnit,
|
() => props.timeUnit,
|
||||||
|
() => props.axes?.x?.lineVisible,
|
||||||
|
() => props.axes?.y?.lineVisible,
|
||||||
],
|
],
|
||||||
async () => {
|
async () => {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
@@ -307,6 +322,7 @@ watch(
|
|||||||
v-if="track.showXAxis && !cleanView"
|
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"
|
||||||
|
:class="{ 'waveform-track__axis--line-hidden': axes?.x?.lineVisible === false }"
|
||||||
:transform="`translate(0, ${track.height})`"
|
:transform="`translate(0, ${track.height})`"
|
||||||
/>
|
/>
|
||||||
<g
|
<g
|
||||||
@@ -353,7 +369,10 @@ watch(
|
|||||||
: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"
|
||||||
:class="`waveform-track__axis--${axis.side}`"
|
:class="[
|
||||||
|
`waveform-track__axis--${axis.side}`,
|
||||||
|
{ 'waveform-track__axis--line-hidden': axes?.y?.lineVisible === false },
|
||||||
|
]"
|
||||||
:data-y-axis-index="axis.index"
|
:data-y-axis-index="axis.index"
|
||||||
:data-y-axis-side="axis.side"
|
:data-y-axis-side="axis.side"
|
||||||
:transform="`translate(${axis.x}, 0)`"
|
:transform="`translate(${axis.x}, 0)`"
|
||||||
@@ -439,7 +458,14 @@ watch(
|
|||||||
fill="none"
|
fill="none"
|
||||||
:stroke="resolvedFrameStyle.borderColor"
|
:stroke="resolvedFrameStyle.borderColor"
|
||||||
:stroke-width="resolvedFrameStyle.borderWidth"
|
:stroke-width="resolvedFrameStyle.borderWidth"
|
||||||
:stroke-dasharray="resolvedFrameStyle.borderStyle === 'dashed' ? '6 4' : undefined"
|
:stroke-dasharray="
|
||||||
|
resolvedFrameStyle.borderStyle === 'dashed'
|
||||||
|
? '6 4'
|
||||||
|
: resolvedFrameStyle.borderStyle === 'dotted'
|
||||||
|
? '1 3'
|
||||||
|
: undefined
|
||||||
|
"
|
||||||
|
:stroke-linecap="resolvedFrameStyle.borderStyle === 'dotted' ? 'round' : undefined"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformAxesOptions,
|
||||||
WaveformZeroLineOptions,
|
WaveformZeroLineOptions,
|
||||||
// 数据类型
|
// 数据类型
|
||||||
SingleWaveformData,
|
SingleWaveformData,
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ body {
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.grid-line-control--switch-only {
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto;
|
||||||
|
}
|
||||||
|
|
||||||
.grid-line-color-picker,
|
.grid-line-color-picker,
|
||||||
.grid-line-color-picker .vc-color-wrap {
|
.grid-line-color-picker .vc-color-wrap {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
|
|||||||
@@ -104,6 +104,8 @@ export type WaveformLegendOrientation = 'auto' | 'horizontal' | 'vertical'
|
|||||||
/** Options shared by legends in every multi-series track. */
|
/** Options shared by legends in every multi-series track. */
|
||||||
export interface WaveformLegendOptions {
|
export interface WaveformLegendOptions {
|
||||||
position?: WaveformLegendPosition
|
position?: WaveformLegendPosition
|
||||||
|
/** Per-track position overrides keyed by trackId, or by series id when trackId is omitted. */
|
||||||
|
trackPositions?: Record<string, WaveformLegendPosition>
|
||||||
orientation?: WaveformLegendOrientation
|
orientation?: WaveformLegendOrientation
|
||||||
/** CSS color used by the legend panel; alpha controls background transparency. */
|
/** CSS color used by the legend panel; alpha controls background transparency. */
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
@@ -115,10 +117,20 @@ export interface WaveformLegendOptions {
|
|||||||
export interface WaveformFrameStyle {
|
export interface WaveformFrameStyle {
|
||||||
borderColor?: string
|
borderColor?: string
|
||||||
borderWidth?: number
|
borderWidth?: number
|
||||||
borderStyle?: 'solid' | 'dashed'
|
borderStyle?: 'solid' | 'dashed' | 'dotted'
|
||||||
backgroundColor?: string
|
backgroundColor?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Controls axis baseline visibility while preserving tick marks and axis text. */
|
||||||
|
export interface WaveformAxesOptions {
|
||||||
|
x?: {
|
||||||
|
lineVisible?: boolean
|
||||||
|
}
|
||||||
|
y?: {
|
||||||
|
lineVisible?: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Styling and visibility options for the horizontal zero-value reference line. */
|
/** Styling and visibility options for the horizontal zero-value reference line. */
|
||||||
export interface WaveformZeroLineOptions {
|
export interface WaveformZeroLineOptions {
|
||||||
visible?: boolean
|
visible?: boolean
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export type {
|
|||||||
WaveformLegendOrientation,
|
WaveformLegendOrientation,
|
||||||
WaveformLegendOptions,
|
WaveformLegendOptions,
|
||||||
WaveformFrameStyle,
|
WaveformFrameStyle,
|
||||||
|
WaveformAxesOptions,
|
||||||
WaveformZeroLineOptions,
|
WaveformZeroLineOptions,
|
||||||
} from './chart'
|
} from './chart'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user