Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233b9b0aa2 | ||
|
|
5c71c28429 | ||
|
|
7a6a7e5c26 |
105
README.md
105
README.md
@@ -96,37 +96,37 @@ const data = ref<WaveformData>({
|
|||||||
|
|
||||||
### Props
|
### Props
|
||||||
|
|
||||||
| Prop | 类型 | 默认值 | 说明 |
|
| Prop | 类型 | 默认值 | 说明 |
|
||||||
| --------------------------------- | ------------------------------------------- | ------------------------------------------------------- | ---------------------------------- |
|
| --------------------------------- | ------------------------------------------- | ------------------------------------------------------- | --------------------------------------------- |
|
||||||
| `data` | `WaveformData` | 必填 | 波形数据 |
|
| `data` | `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)` / `'幅值'` | 坐标轴名称 |
|
| `xLabel` / `yLabel` | `string` | `时间(timeUnit)` / `'幅值'` | 坐标轴名称 |
|
||||||
| `lineColor` | `string` | `'#0960bd'` | 单波形默认颜色 |
|
| `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 坐标数 |
|
| `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 配置初始范围(可超出数据) |
|
||||||
| `xDomainStrategy` | `WaveformXDomainStrategy` | `{ type: 'data' }` | 自动 X 轴视口范围策略 |
|
| `xDomainStrategy` | `WaveformXDomainStrategy` | `{ type: 'data' }` | 自动 X 轴视口范围策略 |
|
||||||
| `yDomain` | `[number, number]` | 未设置 | 所有波形的固定 Y 轴范围 |
|
| `yDomain` | `[number, number]` | 未设置 | 所有波形的固定 Y 轴范围 |
|
||||||
| `yDomains` | `Record<string, [number, number]>` | 未设置 | 按 track/series ID 配置固定范围 |
|
| `yDomains` | `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 轴基线显隐与 X 轴 label 格式化 |
|
| `axes` | `WaveformAxesOptions` | 轴线均显示 | X/Y 轴基线显隐与 X 轴 label 格式化 |
|
||||||
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
| `rendering` | `WaveformRenderingOptions` | `{}` | 降采样与点/误差棒间距 |
|
||||||
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
| `title` / `legend` / `frameStyle` | 对应公开类型 | 未设置 | 标题、图例和图框样式 |
|
||||||
| `frameNumber` | `string \| number` | 未设置 | 图框水印内容 |
|
| `frameNumber` | `string \| number` | 未设置 | 图框水印内容 |
|
||||||
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
| `zeroLine` | `WaveformZeroLineOptions` | `{ visible: false }` | 零值参考线显隐与样式 |
|
||||||
| `cleanView` | `boolean` | `false` | 保留波形、图框和刻度的净图模式 |
|
| `cleanView` | `boolean` | `false` | 保留波形、图框和刻度的净图模式 |
|
||||||
| `presentationMode` | `boolean` | `false` | 禁用绘图区交互的展示模式 |
|
| `presentationMode` | `boolean` | `false` | 禁用绘图区交互的展示模式 |
|
||||||
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
| `annotations` | `WaveformAnnotation[]` | `[]` | 受控标注数据 |
|
||||||
| `annotationsVisible` | `boolean` | `true` | 标注图层显隐 |
|
| `annotationsVisible` | `boolean` | `true` | 标注图层显隐 |
|
||||||
| `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 |
|
| `interactionMode` | `'zoom' \| 'annotation'` | `'zoom'` | 左键交互模式 |
|
||||||
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
| `hiddenSeriesIds` | `string[]` | 未设置 | 受控隐藏系列 ID |
|
||||||
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
| `defaultHiddenSeriesIds` | `string[]` | `[]` | 非受控模式的初始隐藏系列 |
|
||||||
|
|
||||||
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
所有公开类型均可从包入口导入,例如 `WaveformData`、`WaveformSeries`、
|
||||||
`WaveformAnnotation`、`WaveformLineStyle`、`WaveformRenderingOptions`、
|
`WaveformAnnotation`、`WaveformLineStyle`、`WaveformRenderingOptions`、
|
||||||
@@ -253,6 +253,20 @@ X 轴且包含多个轨道时使用按稳定 track ID 索引的 `yRanges`。平
|
|||||||
重置组件内部缩放并触发 `zoom-reset`;调用方应在事件中取消区间请求并恢复首次完整数据。
|
重置组件内部缩放并触发 `zoom-reset`;调用方应在事件中取消区间请求并恢复首次完整数据。
|
||||||
外部重置按钮也可以通过模板引用调用组件公开的 `resetViewport()` 方法,然后执行相同的数据恢复逻辑。
|
外部重置按钮也可以通过模板引用调用组件公开的 `resetViewport()` 方法,然后执行相同的数据恢复逻辑。
|
||||||
|
|
||||||
|
宿主在替换局部数据后如果需要恢复之前保存的 X 视口,可以调用公开的
|
||||||
|
`setViewportDomain(domain, trackIndex?)`。传入范围使用原始秒坐标,组件会按当前数据边界、
|
||||||
|
`xDomainStrategy`、`minZoomSpan` 和 `minVisiblePoints` 重新约束;无效范围会被忽略。共享
|
||||||
|
X 轴模式直接传入一个范围,独立模式可以按实际 `trackIndex` 分别设置(省略 `trackIndex`
|
||||||
|
时应用到当前所有图框):
|
||||||
|
|
||||||
|
```ts
|
||||||
|
chartRef.value?.setViewportDomain(previousDomain)
|
||||||
|
chartRef.value?.setViewportDomain(trackDomain, trackIndex)
|
||||||
|
```
|
||||||
|
|
||||||
|
建议在 `data` 引用更新后调用该方法。组件也会在数据引用变更时重投影当前内部视口,避免新
|
||||||
|
数据边界使旧 transform 失效;`resetViewport(trackIndex?)` 的既有行为保持不变。
|
||||||
|
|
||||||
没有显式配置初始范围时,可以通过 `xDomainStrategy` 将数据范围扩展为便于阅读的视口端点。
|
没有显式配置初始范围时,可以通过 `xDomainStrategy` 将数据范围扩展为便于阅读的视口端点。
|
||||||
默认的 `{ type: 'data' }` 保持数据最小值和最大值不变;`type: 'nice'` 使用固定刻度数量计算
|
默认的 `{ type: 'data' }` 保持数据最小值和最大值不变;`type: 'nice'` 使用固定刻度数量计算
|
||||||
易读边界,且只扩展视口,不修改原始点位、tooltip、标注或缩放事件值:
|
易读边界,且只扩展视口,不修改原始点位、tooltip、标注或缩放事件值:
|
||||||
@@ -284,6 +298,34 @@ X 轴且包含多个轨道时使用按稳定 track ID 索引的 `yRanges`。平
|
|||||||
Tooltip 每个系列按 `炮号:通道 (x:值 y:值)` 格式显示。`WaveformSeries.shotNo` 为空或未提供时,
|
Tooltip 每个系列按 `炮号:通道 (x:值 y:值)` 格式显示。`WaveformSeries.shotNo` 为空或未提供时,
|
||||||
炮号显示为“未配置炮号”;Tooltip 不显示单位和误差附加文本。
|
炮号显示为“未配置炮号”;Tooltip 不显示单位和误差附加文本。
|
||||||
|
|
||||||
|
### 纵轴单位
|
||||||
|
|
||||||
|
可在每个 `WaveformSeries` 上设置可选的 `unit`,单位会显示在对应 Y 轴的顶部刻度中:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const chartData = {
|
||||||
|
kind: 'series',
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: 'voltage',
|
||||||
|
name: '电压',
|
||||||
|
unit: 'V',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 1200 },
|
||||||
|
{ x: 1, y: 3000 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
} satisfies WaveformData
|
||||||
|
```
|
||||||
|
|
||||||
|
当 Y 轴使用科学计数法时,顶部刻度格式为 `E+03 (V) 3`;未使用科学计数法时,格式为
|
||||||
|
`(V) 3`。单位为空、全为空白或未配置时,不显示 `(单位)`,例如仍显示为 `E+03 3` 或 `3`。
|
||||||
|
多 Y 轴模式下,每根轴使用该轴首个 series 的单位。
|
||||||
|
|
||||||
### 线型、点型与误差棒
|
### 线型、点型与误差棒
|
||||||
|
|
||||||
每条序列可以独立设置连线方式、数据点符号和误差棒:
|
每条序列可以独立设置连线方式、数据点符号和误差棒:
|
||||||
@@ -415,7 +457,8 @@ Demo 左侧控制面板提供标题实时预览,可配置标题名称、显隐
|
|||||||
|
|
||||||
### 图例与曲线显隐
|
### 图例与曲线显隐
|
||||||
|
|
||||||
`legend.backgroundColor` 设置多曲线图例的背景颜色。该字段接受任意有效 CSS 颜色值,
|
每个图框独立管理自己的图例:图框内有两条或更多曲线时显示图例,只有一条曲线时不显示。
|
||||||
|
`legend.backgroundColor` 设置图例的背景颜色。该字段接受任意有效 CSS 颜色值,
|
||||||
可通过 `rgba(...)` 或 `hsla(...)` 中的 alpha 通道调整透明度:
|
可通过 `rgba(...)` 或 `hsla(...)` 中的 alpha 通道调整透明度:
|
||||||
|
|
||||||
```vue
|
```vue
|
||||||
@@ -654,7 +697,7 @@ async function importAnnotationFile(file: File) {
|
|||||||
字段无效时会抛出 `TypeError`,不会返回部分结果。导入包含未知 `seriesId` 的标注是允许的,
|
字段无效时会抛出 `TypeError`,不会返回部分结果。导入包含未知 `seriesId` 的标注是允许的,
|
||||||
对应曲线加载后会恢复显示。文件选择、错误提示和下载由业务层实现。
|
对应曲线加载后会恢复显示。文件选择、错误提示和下载由业务层实现。
|
||||||
|
|
||||||
X 轴刻度和左右端点先按 `timeUnit` 转换为秒或毫秒,再显示为不带分组符和科学计数法的完整普通十进制值,并可通过 `axes.x.labelFormatter` 自定义。Y 轴会根据完整显示域选择格式:最大绝对值在 `[0.01, 100)` 时显示两位普通小数;大于等于 `100`,或大于 `0` 且小于 `0.01` 时,刻度显示两位缩放值,并在轴末端单独显示共享倍率 `E±NN`,多 Y 轴分别计算倍率。tooltip 使用最多 4 位小数的本地化普通数字并省略无意义尾零;标注编辑器的 X 坐标跟随 `timeUnit` 并固定 3 位小数,Y 坐标显示完整普通十进制。所有格式化都只发生在展示层,内部坐标值保持原始精度。
|
X 轴刻度和左右端点先按 `timeUnit` 转换为秒或毫秒,再显示为不带分组符和科学计数法的完整普通十进制值,并可通过 `axes.x.labelFormatter` 自定义。Y 轴会根据完整显示域选择格式:最大绝对值在 `[0.01, 100)` 时显示两位普通小数;大于等于 `100`,或大于 `0` 且小于 `0.01` 时,刻度显示两位缩放值,并在顶部刻度单独显示共享倍率 `E±NN`。如果 series 配置了 `unit`,顶部刻度会在倍率后追加 `(单位)`;没有科学计数法时也会显示 `(单位)`,多 Y 轴分别使用对应轴首个 series 的单位。tooltip 使用最多 4 位小数的本地化普通数字并省略无意义尾零;标注编辑器的 X 坐标跟随 `timeUnit` 并固定 3 位小数,Y 坐标显示完整普通十进制。所有格式化都只发生在展示层,内部坐标值保持原始精度。
|
||||||
标注框默认布局在采样点正上方,只做绘图区边界裁剪;文本框通过连接箭头指向标注位置,多个标注重叠时可通过拖动手动避让。
|
标注框默认布局在采样点正上方,只做绘图区边界裁剪;文本框通过连接箭头指向标注位置,多个标注重叠时可通过拖动手动避让。
|
||||||
|
|
||||||
## 事件
|
## 事件
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "waveform-analysis",
|
"name": "waveform-analysis",
|
||||||
"version": "0.1.37",
|
"version": "0.1.40",
|
||||||
"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",
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
tracks.map((track) =>
|
tracks.map((track) =>
|
||||||
track.findAll('.waveform-chart__series').map((item) => item.attributes('data-series-name')),
|
track.findAll('.waveform-chart__series').map((item) => item.attributes('data-series-name')),
|
||||||
),
|
),
|
||||||
).toEqual([['正弦基波'], ['谐波扰动'], ['阻尼振荡'], ['阶跃响应']])
|
).toEqual([['正弦基波'], ['谐波扰动', '谐波对比'], ['阻尼振荡'], ['阶跃响应']])
|
||||||
expect(
|
expect(
|
||||||
tracks
|
tracks
|
||||||
.slice(1)
|
.slice(1)
|
||||||
@@ -253,6 +253,7 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
expect(simulatedSeries.map((item) => item.name)).toEqual([
|
expect(simulatedSeries.map((item) => item.name)).toEqual([
|
||||||
'正弦基波',
|
'正弦基波',
|
||||||
'谐波扰动',
|
'谐波扰动',
|
||||||
|
'谐波对比',
|
||||||
'阻尼振荡',
|
'阻尼振荡',
|
||||||
'阶跃响应',
|
'阶跃响应',
|
||||||
'脉冲响应',
|
'脉冲响应',
|
||||||
@@ -321,7 +322,6 @@ describe('App workspace layout', { timeout: 20_000 }, () => {
|
|||||||
const styleSelect = frameControls.findAllComponents(Select)[0]
|
const styleSelect = frameControls.findAllComponents(Select)[0]
|
||||||
|
|
||||||
expect(colorPickers).toHaveLength(2)
|
expect(colorPickers).toHaveLength(2)
|
||||||
|
|
||||||
const initialFrames = wrapper.findAll('.waveform-chart__plot-frame')
|
const initialFrames = wrapper.findAll('.waveform-chart__plot-frame')
|
||||||
expect(initialFrames.length > 1 && initialFrames.every((frame) => frame.attributes('stroke-width') === '2')).toBe(true)
|
expect(initialFrames.length > 1 && initialFrames.every((frame) => frame.attributes('stroke-width') === '2')).toBe(true)
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ const controller = useWaveformChartController(props as ResolvedWaveformChartProp
|
|||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
resetViewport: controller.resetViewport,
|
resetViewport: controller.resetViewport,
|
||||||
|
setViewportDomain: controller.setViewportDomain,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ export function axisTextMetrics(
|
|||||||
domain: [number, number],
|
domain: [number, number],
|
||||||
nice = true,
|
nice = true,
|
||||||
tickValues?: number[],
|
tickValues?: number[],
|
||||||
|
unit?: string,
|
||||||
): { tickTextWidth: number } {
|
): { tickTextWidth: number } {
|
||||||
const scale = scaleLinear(domain, [1, 0])
|
const scale = scaleLinear(domain, [1, 0])
|
||||||
if (nice) scale.nice()
|
if (nice) scale.nice()
|
||||||
@@ -112,7 +113,7 @@ export function axisTextMetrics(
|
|||||||
const maximumTickCharacters = Math.max(
|
const maximumTickCharacters = Math.max(
|
||||||
1,
|
1,
|
||||||
...values.map(
|
...values.map(
|
||||||
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
|
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue, unit }).length,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
@@ -122,7 +123,8 @@ export function axisTextMetrics(
|
|||||||
|
|
||||||
export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
||||||
return (
|
return (
|
||||||
axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
|
axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
||||||
|
.tickTextWidth +
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
Y_AXIS_LABEL_GAP +
|
Y_AXIS_LABEL_GAP +
|
||||||
Y_AXIS_LABEL_BAND_WIDTH +
|
Y_AXIS_LABEL_BAND_WIDTH +
|
||||||
@@ -132,7 +134,8 @@ export function measureYAxisGroupClearance(group: YAxisSeriesGroup): number {
|
|||||||
|
|
||||||
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
|
function measureYAxisGroupTickClearance(group: YAxisSeriesGroup): number {
|
||||||
return (
|
return (
|
||||||
axisTextMetrics(group.domain, !group.fixed).tickTextWidth +
|
axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
||||||
|
.tickTextWidth +
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
Y_AXIS_OUTER_PADDING
|
Y_AXIS_OUTER_PADDING
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -135,7 +135,12 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
const tickValues = Array.from(
|
const tickValues = Array.from(
|
||||||
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
new Set([axisStart, ...visibleMajorTicks, ...(showAxisEnd ? [axisEnd] : [])]),
|
||||||
)
|
)
|
||||||
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
const { tickTextWidth } = axisTextMetrics(
|
||||||
|
group.domain,
|
||||||
|
!group.fixed,
|
||||||
|
tickValues,
|
||||||
|
group.seriesList[0]?.unit,
|
||||||
|
)
|
||||||
const clearance =
|
const clearance =
|
||||||
tickTextWidth +
|
tickTextWidth +
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
|
|||||||
@@ -259,6 +259,9 @@ export function useWaveformChartController(
|
|||||||
trackLayouts,
|
trackLayouts,
|
||||||
innerWidth,
|
innerWidth,
|
||||||
innerHeight,
|
innerHeight,
|
||||||
|
sharedZoomDomain,
|
||||||
|
initialXDomain,
|
||||||
|
sharedTransform,
|
||||||
activeInteractionMode,
|
activeInteractionMode,
|
||||||
hiddenSeriesIdSet,
|
hiddenSeriesIdSet,
|
||||||
internalHiddenSeriesIds,
|
internalHiddenSeriesIds,
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ interface LifecycleContext {
|
|||||||
trackLayouts: ComputedRef<TrackLayout[]>
|
trackLayouts: ComputedRef<TrackLayout[]>
|
||||||
innerWidth: ComputedRef<number>
|
innerWidth: ComputedRef<number>
|
||||||
innerHeight: ComputedRef<number>
|
innerHeight: ComputedRef<number>
|
||||||
|
sharedZoomDomain: ComputedRef<[number, number]>
|
||||||
|
initialXDomain: ComputedRef<[number, number]>
|
||||||
|
sharedTransform: ShallowRef<ZoomTransform>
|
||||||
activeInteractionMode: ComputedRef<string | undefined>
|
activeInteractionMode: ComputedRef<string | undefined>
|
||||||
hiddenSeriesIdSet: ComputedRef<Set<string>>
|
hiddenSeriesIdSet: ComputedRef<Set<string>>
|
||||||
internalHiddenSeriesIds: Ref<Set<string>>
|
internalHiddenSeriesIds: Ref<Set<string>>
|
||||||
@@ -93,6 +96,9 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|||||||
trackLayouts,
|
trackLayouts,
|
||||||
innerWidth,
|
innerWidth,
|
||||||
innerHeight,
|
innerHeight,
|
||||||
|
sharedZoomDomain,
|
||||||
|
initialXDomain,
|
||||||
|
sharedTransform,
|
||||||
activeInteractionMode,
|
activeInteractionMode,
|
||||||
hiddenSeriesIdSet,
|
hiddenSeriesIdSet,
|
||||||
internalHiddenSeriesIds,
|
internalHiddenSeriesIds,
|
||||||
@@ -144,25 +150,24 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|||||||
emit('page-change', nextPage, pageCount.value)
|
emit('page-change', nextPage, pageCount.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let pendingSharedXDomain: [number, number] | undefined
|
||||||
let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
|
let pendingIndependentXDomains: Array<[number, number] | undefined> | undefined
|
||||||
|
|
||||||
function handleBeforeDataReferenceChange() {
|
function handleBeforeDataReferenceChange() {
|
||||||
if (props.displayMode !== 'independent') return
|
if (props.displayMode === 'independent') {
|
||||||
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
pendingSharedXDomain = undefined
|
||||||
const configuredDomain =
|
pendingIndependentXDomains = trackLayouts.value.map((track) => {
|
||||||
props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
|
const current = track.xScale.domain() as [number, number]
|
||||||
props.initialXDomains?.[track.series.id] ??
|
const boundary = resolveInitialTrackDomain(track)
|
||||||
props.initialXDomain
|
return current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? current : undefined
|
||||||
if (
|
})
|
||||||
!configuredDomain ||
|
return
|
||||||
!Number.isFinite(configuredDomain[0]) ||
|
}
|
||||||
!Number.isFinite(configuredDomain[1]) ||
|
pendingIndependentXDomains = undefined
|
||||||
configuredDomain[0] === configuredDomain[1]
|
const current = sharedZoomDomain.value
|
||||||
) {
|
const boundary = initialXDomain.value
|
||||||
return undefined
|
pendingSharedXDomain =
|
||||||
}
|
current[1] - current[0] < boundary[1] - boundary[0] - 1e-12 ? [...current] : undefined
|
||||||
return track.xScale.domain() as [number, number]
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDataReferenceChange() {
|
function handleDataReferenceChange() {
|
||||||
@@ -186,10 +191,15 @@ export function useWaveformChartLifecycle(context: LifecycleContext) {
|
|||||||
})
|
})
|
||||||
independentTransforms.value = nextTransforms
|
independentTransforms.value = nextTransforms
|
||||||
pendingIndependentXDomains = undefined
|
pendingIndependentXDomains = undefined
|
||||||
void nextTick(configureZoom)
|
} else if (props.displayMode !== 'independent' && pendingSharedXDomain) {
|
||||||
return
|
const boundary = initialXDomain.value
|
||||||
|
const groups = trackLayouts.value
|
||||||
|
.filter((track) => track.hasVisibleSeries)
|
||||||
|
.map((track) => track.seriesList)
|
||||||
|
const domain = constrainZoomDomain(pendingSharedXDomain, boundary, groups, props)
|
||||||
|
sharedTransform.value = transformForDomain(domain, boundary, innerWidth.value)
|
||||||
|
pendingSharedXDomain = undefined
|
||||||
}
|
}
|
||||||
pendingIndependentXDomains = undefined
|
|
||||||
configureZoom()
|
configureZoom()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ export function useWaveformLayout(context: LayoutContext) {
|
|||||||
.flatMap((track) =>
|
.flatMap((track) =>
|
||||||
resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
|
resolveYAxisSeriesGroups(track, props.overlayMode, props.yDomain, props.yDomains),
|
||||||
)
|
)
|
||||||
.map((group) => axisTextMetrics(group.domain, !group.fixed).tickTextWidth)
|
.map(
|
||||||
|
(group) =>
|
||||||
|
axisTextMetrics(group.domain, !group.fixed, undefined, group.seriesList[0]?.unit)
|
||||||
|
.tickTextWidth,
|
||||||
|
)
|
||||||
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
||||||
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
||||||
const labelCenterX = -(
|
const labelCenterX = -(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import type {
|
|||||||
import type { AnnotationSeriesCandidate } from '../annotation'
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
||||||
import { tryReleasePointerCapture } from './pointerCapture'
|
import { tryReleasePointerCapture } from './pointerCapture'
|
||||||
import { transitionViewportInteraction } from './viewportInteractionState'
|
import { transitionViewportInteraction } from './viewportInteractionState'
|
||||||
|
import { createViewportDomainSetter } from './viewportDomain'
|
||||||
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
||||||
interface ViewportContext {
|
interface ViewportContext {
|
||||||
props: ResolvedWaveformChartProps
|
props: ResolvedWaveformChartProps
|
||||||
@@ -344,6 +345,20 @@ export function useWaveformViewport(context: ViewportContext) {
|
|||||||
editorSeriesOptions.value = []
|
editorSeriesOptions.value = []
|
||||||
void nextTick(configureZoom)
|
void nextTick(configureZoom)
|
||||||
}
|
}
|
||||||
|
const setViewportDomain = createViewportDomainSetter({
|
||||||
|
props,
|
||||||
|
trackLayouts,
|
||||||
|
initialXDomain,
|
||||||
|
resolveInitialTrackDomain,
|
||||||
|
innerWidth,
|
||||||
|
sharedTransform,
|
||||||
|
independentTransforms,
|
||||||
|
cancelPendingZoom,
|
||||||
|
cancelViewportDrag,
|
||||||
|
clearHover,
|
||||||
|
editorSeriesOptions,
|
||||||
|
configureZoom,
|
||||||
|
})
|
||||||
const requestViewportReset = (event: MouseEvent) => {
|
const requestViewportReset = (event: MouseEvent) => {
|
||||||
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
if (isPresentationMode.value || !props.zoomable || !isZoomMode.value) return
|
||||||
if (props.displayMode === 'independent') {
|
if (props.displayMode === 'independent') {
|
||||||
@@ -372,6 +387,7 @@ export function useWaveformViewport(context: ViewportContext) {
|
|||||||
finishViewportDrag,
|
finishViewportDrag,
|
||||||
cancelViewportDrag,
|
cancelViewportDrag,
|
||||||
resetViewport,
|
resetViewport,
|
||||||
|
setViewportDomain,
|
||||||
requestViewportReset,
|
requestViewportReset,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
60
src/components/interaction/viewportDomain.ts
Normal file
60
src/components/interaction/viewportDomain.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { nextTick, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
||||||
|
import type { ZoomTransform } from 'd3'
|
||||||
|
|
||||||
|
import type { TrackLayout } from '../core/types'
|
||||||
|
import type { ResolvedWaveformChartProps } from '../core/waveformChartTypes'
|
||||||
|
import type { AnnotationSeriesCandidate } from '../annotation'
|
||||||
|
import { constrainZoomDomain, transformForDomain } from './zoomConstraints'
|
||||||
|
|
||||||
|
interface ViewportDomainContext {
|
||||||
|
props: ResolvedWaveformChartProps
|
||||||
|
trackLayouts: ComputedRef<TrackLayout[]>
|
||||||
|
initialXDomain: ComputedRef<[number, number]>
|
||||||
|
resolveInitialTrackDomain: (track: TrackLayout) => [number, number]
|
||||||
|
innerWidth: ComputedRef<number>
|
||||||
|
sharedTransform: ShallowRef<ZoomTransform>
|
||||||
|
independentTransforms: ShallowRef<ZoomTransform[]>
|
||||||
|
cancelPendingZoom: () => void
|
||||||
|
cancelViewportDrag: () => void
|
||||||
|
clearHover: () => void
|
||||||
|
editorSeriesOptions: Ref<AnnotationSeriesCandidate[]>
|
||||||
|
configureZoom: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createViewportDomainSetter(context: ViewportDomainContext) {
|
||||||
|
return (domain: [number, number], trackIndex?: number) => {
|
||||||
|
if (!Number.isFinite(domain[0]) || !Number.isFinite(domain[1]) || domain[0] === domain[1])
|
||||||
|
return
|
||||||
|
context.cancelPendingZoom()
|
||||||
|
context.cancelViewportDrag()
|
||||||
|
if (context.props.displayMode === 'independent') {
|
||||||
|
const indexes =
|
||||||
|
trackIndex === undefined
|
||||||
|
? context.trackLayouts.value.map((track) => track.index)
|
||||||
|
: [trackIndex]
|
||||||
|
const nextTransforms = [...context.independentTransforms.value]
|
||||||
|
indexes.forEach((index) => {
|
||||||
|
const track = context.trackLayouts.value.find((item) => item.index === index)
|
||||||
|
if (!track) return
|
||||||
|
const boundary = context.resolveInitialTrackDomain(track)
|
||||||
|
const constrained = constrainZoomDomain(domain, boundary, [track.seriesList], context.props)
|
||||||
|
nextTransforms[index] = transformForDomain(constrained, boundary, track.width)
|
||||||
|
})
|
||||||
|
context.independentTransforms.value = nextTransforms
|
||||||
|
} else {
|
||||||
|
const boundary = context.initialXDomain.value
|
||||||
|
const groups = context.trackLayouts.value
|
||||||
|
.filter((track) => track.hasVisibleSeries)
|
||||||
|
.map((track) => track.seriesList)
|
||||||
|
const constrained = constrainZoomDomain(domain, boundary, groups, context.props)
|
||||||
|
context.sharedTransform.value = transformForDomain(
|
||||||
|
constrained,
|
||||||
|
boundary,
|
||||||
|
context.innerWidth.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
context.clearHover()
|
||||||
|
context.editorSeriesOptions.value = []
|
||||||
|
void nextTick(context.configureZoom)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -44,9 +44,10 @@ function renderAxes() {
|
|||||||
if (!element) return
|
if (!element) return
|
||||||
const [axisMin, axisMax] = axis.scale.domain()
|
const [axisMin, axisMax] = axis.scale.domain()
|
||||||
const topTickValue = Math.max(...axis.tickValues)
|
const topTickValue = Math.max(...axis.tickValues)
|
||||||
|
const unit = axis.seriesList[0]?.unit
|
||||||
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
||||||
.tickFormat((value) =>
|
.tickFormat((value) =>
|
||||||
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue }),
|
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue, unit }),
|
||||||
)
|
)
|
||||||
.tickSize(-4)
|
.tickSize(-4)
|
||||||
.tickPadding(7)
|
.tickPadding(7)
|
||||||
|
|||||||
@@ -158,6 +158,12 @@ describe('WaveformChart', () => {
|
|||||||
expect(paths[0].attributes('data-series-name')).toBe('BT2_2M')
|
expect(paths[0].attributes('data-series-name')).toBe('BT2_2M')
|
||||||
const yAxisLabels = wrapper.findAll('.waveform-chart__y-axis-label')
|
const yAxisLabels = wrapper.findAll('.waveform-chart__y-axis-label')
|
||||||
expect(yAxisLabels.map((label) => label.text())).toEqual(['BT2_2M', 'BT1_2M'])
|
expect(yAxisLabels.map((label) => label.text())).toEqual(['BT2_2M', 'BT1_2M'])
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.findAll('.waveform-chart__axis--y .tick text')
|
||||||
|
.map((label) => label.text())
|
||||||
|
.some((label) => label.includes('(T)')),
|
||||||
|
).toBe(true)
|
||||||
expect(yAxisLabels.map((label) => label.attributes('fill'))).toEqual(['#0960bd', '#ff7f0e'])
|
expect(yAxisLabels.map((label) => label.attributes('fill'))).toEqual(['#0960bd', '#ff7f0e'])
|
||||||
const labelX = Number(
|
const labelX = Number(
|
||||||
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
yAxisLabels[0].attributes('transform')?.match(/^translate\(([-\d.]+),/)?.[1],
|
||||||
|
|||||||
@@ -16,13 +16,30 @@ function annotationEditorExists() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('WaveformChart', () => {
|
describe('WaveformChart', () => {
|
||||||
|
it('shows legends when a track contains at least two series', async () => {
|
||||||
|
const oneSeries = visibilitySeries()
|
||||||
|
if (oneSeries.kind === 'series') oneSeries.series = oneSeries.series.slice(0, 1)
|
||||||
|
const withoutLegend = await mountSizedChart(oneSeries, {
|
||||||
|
grid: { rowCount: 1, columnCount: 1 },
|
||||||
|
})
|
||||||
|
expect(withoutLegend.findAll('.waveform-chart__legend')).toHaveLength(0)
|
||||||
|
|
||||||
|
const twoSeries = visibilitySeries()
|
||||||
|
if (twoSeries.kind === 'series') twoSeries.series = twoSeries.series.slice(0, 2)
|
||||||
|
const withLegend = await mountSizedChart(twoSeries, {
|
||||||
|
grid: { rowCount: 1, columnCount: 1 },
|
||||||
|
})
|
||||||
|
expect(withLegend.findAll('.waveform-chart__legend')).toHaveLength(1)
|
||||||
|
expect(withLegend.find('.waveform-chart__legend').attributes('data-position')).toBe('top-right')
|
||||||
|
})
|
||||||
|
|
||||||
it('resolves legend positions by stable track id across pages', async () => {
|
it('resolves legend positions by stable track id across pages', async () => {
|
||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
kind: 'series',
|
kind: 'series',
|
||||||
series: Array.from({ length: 8 }, (_, index) => ({
|
series: Array.from({ length: 12 }, (_, index) => ({
|
||||||
id: `series-${index}`,
|
id: `series-${index}`,
|
||||||
trackId: `frame-${Math.floor(index / 2)}`,
|
trackId: `frame-${Math.floor(index / 3)}`,
|
||||||
name: `series ${index}`,
|
name: `series ${index}`,
|
||||||
data: {
|
data: {
|
||||||
kind: 'points' as const,
|
kind: 'points' as const,
|
||||||
@@ -75,9 +92,9 @@ describe('WaveformChart', () => {
|
|||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
kind: 'series',
|
kind: 'series',
|
||||||
series: Array.from({ length: 4 }, (_, index) => ({
|
series: Array.from({ length: 6 }, (_, index) => ({
|
||||||
id: `series-${index}`,
|
id: `series-${index}`,
|
||||||
trackId: `frame-${Math.floor(index / 2)}`,
|
trackId: `frame-${Math.floor(index / 3)}`,
|
||||||
name: `series ${index}`,
|
name: `series ${index}`,
|
||||||
data: {
|
data: {
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
@@ -112,7 +129,7 @@ describe('WaveformChart', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const items = wrapper.findAll('.waveform-chart__legend-item')
|
const items = wrapper.findAll('.waveform-chart__legend-item')
|
||||||
expect(items).toHaveLength(2)
|
expect(items).toHaveLength(3)
|
||||||
expect(items.every((item) => item.attributes('disabled') !== undefined)).toBe(true)
|
expect(items.every((item) => item.attributes('disabled') !== undefined)).toBe(true)
|
||||||
expect(wrapper.get('.waveform-legend__panel').classes()).not.toContain(
|
expect(wrapper.get('.waveform-legend__panel').classes()).not.toContain(
|
||||||
'waveform-legend__panel--interactive',
|
'waveform-legend__panel--interactive',
|
||||||
@@ -128,7 +145,7 @@ describe('WaveformChart', () => {
|
|||||||
overlayMode: 'multi-axis',
|
overlayMode: 'multi-axis',
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
|
expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(3)
|
||||||
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
|
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
|
||||||
const annotationLayer = wrapper.get('.waveform-annotation-layer').element
|
const annotationLayer = wrapper.get('.waveform-annotation-layer').element
|
||||||
const legendLayer = wrapper.get('.waveform-chart__legend-layer').element
|
const legendLayer = wrapper.get('.waveform-chart__legend-layer').element
|
||||||
@@ -143,8 +160,8 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['low'])
|
).toEqual(['low', 'mid'])
|
||||||
expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(1)
|
expect(wrapper.findAll('.waveform-chart__axis--y')).toHaveLength(2)
|
||||||
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(false)
|
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(false)
|
||||||
expect(wrapper.findAll('.waveform-chart__legend-item')[1].classes()).toContain('is-hidden')
|
expect(wrapper.findAll('.waveform-chart__legend-item')[1].classes()).toContain('is-hidden')
|
||||||
expect(wrapper.findAll('.waveform-chart__legend-item')[1].attributes('aria-pressed')).toBe(
|
expect(wrapper.findAll('.waveform-chart__legend-item')[1].attributes('aria-pressed')).toBe(
|
||||||
@@ -170,12 +187,12 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
flushAnimationFrames()
|
flushAnimationFrames()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(1)
|
expect(wrapper.findAll('.waveform-chart__tooltip-series')).toHaveLength(2)
|
||||||
expect(wrapper.get('.waveform-chart__tooltip-series').text()).toContain('低量程')
|
expect(wrapper.get('.waveform-chart__tooltip-series').text()).toContain('低量程')
|
||||||
|
|
||||||
await wrapper.findAll('.waveform-chart__legend-item')[1].trigger('click')
|
await wrapper.findAll('.waveform-chart__legend-item')[1].trigger('click')
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
|
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
|
||||||
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
|
expect(wrapper.find('[data-annotation-id="high-note"]').exists()).toBe(true)
|
||||||
expect(wrapper.emitted('series-visibility-change')?.at(-1)).toEqual([
|
expect(wrapper.emitted('series-visibility-change')?.at(-1)).toEqual([
|
||||||
{ seriesId: 'high', visible: true, hiddenSeriesIds: [] },
|
{ seriesId: 'high', visible: true, hiddenSeriesIds: [] },
|
||||||
@@ -191,20 +208,20 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['low'])
|
).toEqual(['low', 'mid'])
|
||||||
await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
|
await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
|
||||||
expect(wrapper.emitted('update:hidden-series-ids')?.at(-1)).toEqual([
|
expect(wrapper.emitted('update:hidden-series-ids')?.at(-1)).toEqual([
|
||||||
['high', 'temporarily-absent', 'low'],
|
['high', 'temporarily-absent', 'low'],
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['low'])
|
).toEqual(['low', 'mid'])
|
||||||
|
|
||||||
await wrapper.setProps({ hiddenSeriesIds: ['low'] })
|
await wrapper.setProps({ hiddenSeriesIds: ['low'] })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['high'])
|
).toEqual(['high', 'mid'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('retains uncontrolled visibility by stable ID and clears removed IDs', async () => {
|
it('retains uncontrolled visibility by stable ID and clears removed IDs', async () => {
|
||||||
@@ -223,7 +240,7 @@ describe('WaveformChart', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
wrapper.findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['low'])
|
).toEqual(['mid', 'low'])
|
||||||
|
|
||||||
await wrapper.setProps({
|
await wrapper.setProps({
|
||||||
data: {
|
data: {
|
||||||
@@ -234,12 +251,12 @@ describe('WaveformChart', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
await wrapper.setProps({ data: original })
|
await wrapper.setProps({ data: original })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(2)
|
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(3)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps a recoverable legend and stops chart interaction when every series is hidden', async () => {
|
it('keeps a recoverable legend and stops chart interaction when every series is hidden', async () => {
|
||||||
const wrapper = await mountSizedChart(visibilitySeries(), {
|
const wrapper = await mountSizedChart(visibilitySeries(), {
|
||||||
defaultHiddenSeriesIds: ['low', 'high'],
|
defaultHiddenSeriesIds: ['low', 'high', 'mid'],
|
||||||
grid: { rowCount: 1, columnCount: 1 },
|
grid: { rowCount: 1, columnCount: 1 },
|
||||||
legend: { interactive: true },
|
legend: { interactive: true },
|
||||||
overlayMode: 'multi-axis',
|
overlayMode: 'multi-axis',
|
||||||
@@ -248,7 +265,7 @@ describe('WaveformChart', () => {
|
|||||||
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
|
expect(wrapper.findAll('.waveform-chart__line')).toHaveLength(0)
|
||||||
expect(wrapper.findAll('.waveform-chart__axis')).toHaveLength(0)
|
expect(wrapper.findAll('.waveform-chart__axis')).toHaveLength(0)
|
||||||
expect(wrapper.findAll('.waveform-chart__overlay')).toHaveLength(0)
|
expect(wrapper.findAll('.waveform-chart__overlay')).toHaveLength(0)
|
||||||
expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(2)
|
expect(wrapper.findAll('.waveform-chart__legend-item')).toHaveLength(3)
|
||||||
expect(wrapper.get('.waveform-track__no-visible-series').text()).toBe('暂无可见曲线')
|
expect(wrapper.get('.waveform-track__no-visible-series').text()).toBe('暂无可见曲线')
|
||||||
|
|
||||||
await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
|
await wrapper.findAll('.waveform-chart__legend-item')[0].trigger('click')
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ describe('WaveformChart', () => {
|
|||||||
{
|
{
|
||||||
trackId: 'shared-frame',
|
trackId: 'shared-frame',
|
||||||
name: '普通量程',
|
name: '普通量程',
|
||||||
|
unit: 'V',
|
||||||
data: {
|
data: {
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
points: [
|
points: [
|
||||||
@@ -26,6 +27,7 @@ describe('WaveformChart', () => {
|
|||||||
{
|
{
|
||||||
trackId: 'shared-frame',
|
trackId: 'shared-frame',
|
||||||
name: '大量程',
|
name: '大量程',
|
||||||
|
unit: 'A',
|
||||||
data: {
|
data: {
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
points: [
|
points: [
|
||||||
@@ -37,6 +39,7 @@ describe('WaveformChart', () => {
|
|||||||
{
|
{
|
||||||
trackId: 'shared-frame',
|
trackId: 'shared-frame',
|
||||||
name: '小量程',
|
name: '小量程',
|
||||||
|
unit: 'T',
|
||||||
data: {
|
data: {
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
points: [
|
points: [
|
||||||
@@ -57,7 +60,11 @@ describe('WaveformChart', () => {
|
|||||||
.map((label) => label.text())
|
.map((label) => label.text())
|
||||||
.filter((label) => label.startsWith('E')),
|
.filter((label) => label.startsWith('E')),
|
||||||
),
|
),
|
||||||
).toEqual([[], [expect.stringMatching(/^E\+03 /)], [expect.stringMatching(/^E-04 /)]])
|
).toEqual([
|
||||||
|
[],
|
||||||
|
[expect.stringMatching(/^E\+03 \(A\) /)],
|
||||||
|
[expect.stringMatching(/^E-04 \(T\) /)],
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
||||||
@@ -177,6 +184,18 @@ describe('WaveformChart', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'reference',
|
||||||
|
trackId: 'frame-1',
|
||||||
|
name: 'REF_CH_1',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 0.25 },
|
||||||
|
{ x: 1, y: 1.25 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ frameNumber: 1, grid: { rowCount: 2, columnCount: 1 } },
|
{ frameNumber: 1, grid: { rowCount: 2, columnCount: 1 } },
|
||||||
@@ -186,7 +205,7 @@ describe('WaveformChart', () => {
|
|||||||
expect(tracks).toHaveLength(2)
|
expect(tracks).toHaveLength(2)
|
||||||
expect(
|
expect(
|
||||||
tracks[0].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
tracks[0].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['primary', 'comparison'])
|
).toEqual(['primary', 'comparison', 'reference'])
|
||||||
expect(
|
expect(
|
||||||
tracks[1].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
tracks[1].findAll('.waveform-chart__line').map((line) => line.attributes('data-series-id')),
|
||||||
).toEqual(['second-frame'])
|
).toEqual(['second-frame'])
|
||||||
@@ -203,12 +222,13 @@ describe('WaveformChart', () => {
|
|||||||
expect(legend.findAll('.waveform-chart__legend-item').map((item) => item.text())).toEqual([
|
expect(legend.findAll('.waveform-chart__legend-item').map((item) => item.text())).toEqual([
|
||||||
'BT2_2M',
|
'BT2_2M',
|
||||||
'TEST_CH_1',
|
'TEST_CH_1',
|
||||||
|
'REF_CH_1',
|
||||||
])
|
])
|
||||||
expect(
|
expect(
|
||||||
legend
|
legend
|
||||||
.findAll('.waveform-legend__swatch')
|
.findAll('.waveform-legend__swatch')
|
||||||
.map((swatch) => swatch.get('path').attributes('stroke')),
|
.map((swatch) => swatch.get('path').attributes('stroke')),
|
||||||
).toEqual(['#0960bd', '#2ca02c'])
|
).toEqual(['#0960bd', '#2ca02c', '#d62728'])
|
||||||
expect(wrapper.findAll('.waveform-chart__watermark').map((item) => item.text())).toEqual([
|
expect(wrapper.findAll('.waveform-chart__watermark').map((item) => item.text())).toEqual([
|
||||||
'1',
|
'1',
|
||||||
'2',
|
'2',
|
||||||
@@ -232,10 +252,11 @@ describe('WaveformChart', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
const tooltipSeries = wrapper.findAll('.waveform-chart__tooltip-series')
|
const tooltipSeries = wrapper.findAll('.waveform-chart__tooltip-series')
|
||||||
expect(tooltipSeries).toHaveLength(2)
|
expect(tooltipSeries).toHaveLength(3)
|
||||||
expect(tooltipSeries.map((item) => item.text())).toEqual([
|
expect(tooltipSeries.map((item) => item.text())).toEqual([
|
||||||
expect.stringContaining('BT2_2M'),
|
expect.stringContaining('BT2_2M'),
|
||||||
expect.stringContaining('TEST_CH_1'),
|
expect.stringContaining('TEST_CH_1'),
|
||||||
|
expect.stringContaining('REF_CH_1'),
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -268,6 +289,18 @@ describe('WaveformChart', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'third',
|
||||||
|
trackId: 'shared',
|
||||||
|
name: 'third',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 2 },
|
||||||
|
{ x: 1, y: 3 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ grid: { rowCount: 1, columnCount: 1 } },
|
{ grid: { rowCount: 1, columnCount: 1 } },
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ const presentationData: WaveformData = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'mid',
|
||||||
|
trackId: 'shared',
|
||||||
|
name: '中量程',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 50 },
|
||||||
|
{ x: 1, y: 100 },
|
||||||
|
{ x: 2, y: 75 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'other',
|
id: 'other',
|
||||||
name: '其他通道',
|
name: '其他通道',
|
||||||
@@ -150,9 +163,9 @@ describe('WaveformChart presentation mode', () => {
|
|||||||
cancelable: true,
|
cancelable: true,
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
wrapper.get('.waveform-chart__svg').element.dispatchEvent(
|
wrapper
|
||||||
new MouseEvent('dblclick', { bubbles: true, cancelable: true }),
|
.get('.waveform-chart__svg')
|
||||||
)
|
.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
|
||||||
await wrapper.get('[data-annotation-id="note"]').trigger('contextmenu', {
|
await wrapper.get('[data-annotation-id="note"]').trigger('contextmenu', {
|
||||||
clientX: width / 2,
|
clientX: width / 2,
|
||||||
clientY: height / 2,
|
clientY: height / 2,
|
||||||
|
|||||||
@@ -119,6 +119,108 @@ describe('WaveformChart viewport reset', () => {
|
|||||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('keeps a controlled shared viewport when the data reference changes', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ displayMode: 'separated' },
|
||||||
|
)
|
||||||
|
const chart = wrapper.vm as unknown as {
|
||||||
|
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||||
|
}
|
||||||
|
chart.setViewportDomain([0.25, 0.75])
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const startBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
||||||
|
const endBefore = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
||||||
|
expect(startBefore).toBeGreaterThan(0)
|
||||||
|
expect(endBefore).toBeLessThan(1000)
|
||||||
|
|
||||||
|
await wrapper.setProps({
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 10 },
|
||||||
|
{ x: 1, y: 11 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())).toBeCloseTo(
|
||||||
|
startBefore,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
expect(Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())).toBeCloseTo(
|
||||||
|
endBefore,
|
||||||
|
10,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps independently controlled track viewports when the data reference changes', async () => {
|
||||||
|
const wrapper = await mountSizedChart(gridSeries(2), {
|
||||||
|
displayMode: 'independent',
|
||||||
|
grid: { rowCount: 1, columnCount: 2 },
|
||||||
|
})
|
||||||
|
const chart = wrapper.vm as unknown as {
|
||||||
|
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||||
|
}
|
||||||
|
chart.setViewportDomain([0.2, 0.8], 0)
|
||||||
|
chart.setViewportDomain([0.1, 0.9], 1)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const endpoints = () =>
|
||||||
|
wrapper.findAll('.waveform-chart__track').map((track) => ({
|
||||||
|
start: track.get('.waveform-chart__axis-endpoint--start').text(),
|
||||||
|
end: track.get('.waveform-chart__axis-endpoint--end').text(),
|
||||||
|
}))
|
||||||
|
const endpointsBefore = endpoints()
|
||||||
|
expect(Number(endpointsBefore[0].start)).toBeGreaterThan(0)
|
||||||
|
expect(Number(endpointsBefore[0].end)).toBeLessThan(1000)
|
||||||
|
expect(Number(endpointsBefore[1].start)).toBeGreaterThan(0)
|
||||||
|
expect(Number(endpointsBefore[1].end)).toBeLessThan(1000)
|
||||||
|
|
||||||
|
await wrapper.setProps({ data: gridSeries(2) })
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const endpointsAfter = endpoints()
|
||||||
|
endpointsAfter.forEach((endpoint, index) => {
|
||||||
|
expect(Number(endpoint.start)).toBeCloseTo(Number(endpointsBefore[index].start), 10)
|
||||||
|
expect(Number(endpoint.end)).toBeCloseTo(Number(endpointsBefore[index].end), 10)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ignores invalid domains and applies viewport constraints', async () => {
|
||||||
|
const wrapper = await mountSizedChart(
|
||||||
|
{
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ displayMode: 'separated', minZoomSpan: 0.2 },
|
||||||
|
)
|
||||||
|
const chart = wrapper.vm as unknown as {
|
||||||
|
setViewportDomain: (domain: [number, number], trackIndex?: number) => void
|
||||||
|
}
|
||||||
|
chart.setViewportDomain([Number.NaN, 0.5])
|
||||||
|
await flushPromises()
|
||||||
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
||||||
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||||
|
|
||||||
|
chart.setViewportDomain([0.49, 0.51])
|
||||||
|
await flushPromises()
|
||||||
|
const start = Number(wrapper.get('.waveform-chart__axis-endpoint--start').text())
|
||||||
|
const end = Number(wrapper.get('.waveform-chart__axis-endpoint--end').text())
|
||||||
|
expect(end - start).toBeGreaterThanOrEqual(200)
|
||||||
|
})
|
||||||
|
|
||||||
it('resets shared and independent explicit domains to their included nice bounds', async () => {
|
it('resets shared and independent explicit domains to their included nice bounds', async () => {
|
||||||
const shared = await mountSizedChart(
|
const shared = await mountSizedChart(
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import { createSimulatedWaveformData } from './simulatedWaveforms'
|
import { createSimulatedWaveformData } from './simulatedWaveforms'
|
||||||
|
|
||||||
describe('simulated waveform data', () => {
|
describe('simulated waveform data', () => {
|
||||||
it('creates deterministic, finite six-channel data', () => {
|
it('creates deterministic, finite seven-channel data with a two-series second frame', () => {
|
||||||
const first = createSimulatedWaveformData()
|
const first = createSimulatedWaveformData()
|
||||||
const second = createSimulatedWaveformData()
|
const second = createSimulatedWaveformData()
|
||||||
|
|
||||||
@@ -11,8 +11,12 @@ describe('simulated waveform data', () => {
|
|||||||
expect(first.kind).toBe('series')
|
expect(first.kind).toBe('series')
|
||||||
if (first.kind !== 'series') return
|
if (first.kind !== 'series') return
|
||||||
|
|
||||||
expect(first.series).toHaveLength(6)
|
expect(first.series).toHaveLength(7)
|
||||||
expect(new Set(first.series.map((series) => series.id)).size).toBe(6)
|
expect(new Set(first.series.map((series) => series.id)).size).toBe(7)
|
||||||
|
const secondFrame = first.series.filter(
|
||||||
|
(series) => series.trackId === 'simulated-harmonic-frame',
|
||||||
|
)
|
||||||
|
expect(secondFrame).toHaveLength(2)
|
||||||
expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
|
expect(new Set(first.series.map((series) => series.shotNo))).toEqual(new Set(['13300']))
|
||||||
first.series.forEach((series) => {
|
first.series.forEach((series) => {
|
||||||
expect(series.data.kind).toBe('points')
|
expect(series.data.kind).toBe('points')
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type ErrorGenerator = (
|
|||||||
|
|
||||||
interface SimulatedSeriesDefinition extends Pick<
|
interface SimulatedSeriesDefinition extends Pick<
|
||||||
WaveformSeries,
|
WaveformSeries,
|
||||||
'id' | 'shotNo' | 'name' | 'unit' | 'lineType' | 'pointType' | 'errorBar'
|
'id' | 'trackId' | 'shotNo' | 'name' | 'unit' | 'color' | 'lineType' | 'pointType' | 'errorBar'
|
||||||
> {
|
> {
|
||||||
signal: SignalGenerator
|
signal: SignalGenerator
|
||||||
errors?: ErrorGenerator
|
errors?: ErrorGenerator
|
||||||
@@ -60,6 +60,7 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'simulated-harmonic',
|
id: 'simulated-harmonic',
|
||||||
|
trackId: 'simulated-harmonic-frame',
|
||||||
shotNo: '13300',
|
shotNo: '13300',
|
||||||
name: '谐波扰动',
|
name: '谐波扰动',
|
||||||
unit: 'V',
|
unit: 'V',
|
||||||
@@ -68,6 +69,18 @@ const seriesDefinitions: SimulatedSeriesDefinition[] = [
|
|||||||
signal: (time) =>
|
signal: (time) =>
|
||||||
0.9 * Math.sin(TWO_PI * 0.55 * time) + 0.28 * Math.sin(TWO_PI * 2.2 * time + 0.4),
|
0.9 * Math.sin(TWO_PI * 0.55 * time) + 0.28 * Math.sin(TWO_PI * 2.2 * time + 0.4),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'simulated-harmonic-reference',
|
||||||
|
trackId: 'simulated-harmonic-frame',
|
||||||
|
shotNo: '13300',
|
||||||
|
name: '谐波对比',
|
||||||
|
unit: 'V',
|
||||||
|
color: '#2ca02c',
|
||||||
|
lineType: 'linear',
|
||||||
|
pointType: 'none',
|
||||||
|
signal: (time) =>
|
||||||
|
0.62 * Math.sin(TWO_PI * 0.55 * time + 0.55) + 0.18 * Math.sin(TWO_PI * 2.2 * time - 0.2),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'simulated-damped',
|
id: 'simulated-damped',
|
||||||
shotNo: '13300',
|
shotNo: '13300',
|
||||||
|
|||||||
@@ -58,6 +58,18 @@ export function visibilitySeries(): WaveformData {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'mid',
|
||||||
|
trackId: 'shared-frame',
|
||||||
|
name: '中量程',
|
||||||
|
data: {
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 100 },
|
||||||
|
{ x: 1, y: 200 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ describe('waveform number formatters', () => {
|
|||||||
expect(formatScientificAxisExponent(0.0001, 0.0003)).toBe('E-04')
|
expect(formatScientificAxisExponent(0.0001, 0.0003)).toBe('E-04')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('appends a unit to the top tick with or without scientific notation', () => {
|
||||||
|
expect(formatScientificAxisLabel(3000, { axisMin: 1000, axisMax: 3000, unit: 'V' })).toBe(
|
||||||
|
'E+03 (V) 3',
|
||||||
|
)
|
||||||
|
expect(formatScientificAxisLabel(3, { axisMin: 0, axisMax: 3, unit: 'A' })).toBe('(A) 3')
|
||||||
|
expect(formatScientificAxisLabel(2, { axisMin: 0, axisMax: 3, unit: 'A' })).toBe('2')
|
||||||
|
expect(formatScientificAxisLabel(3, { axisMin: 0, axisMax: 3, unit: ' ' })).toBe('3')
|
||||||
|
})
|
||||||
|
|
||||||
it('derives the exponent from the largest absolute endpoint', () => {
|
it('derives the exponent from the largest absolute endpoint', () => {
|
||||||
expect(resolveScientificAxisExponent(-9000, -1000)).toBe(3)
|
expect(resolveScientificAxisExponent(-9000, -1000)).toBe(3)
|
||||||
expect(resolveScientificAxisExponent(-100_000, -3000)).toBe(5)
|
expect(resolveScientificAxisExponent(-100_000, -3000)).toBe(5)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface ScientificAxisLabelOptions {
|
|||||||
axisMin?: number
|
axisMin?: number
|
||||||
axisMax?: number
|
axisMax?: number
|
||||||
topTickValue?: number
|
topTickValue?: number
|
||||||
|
unit?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated Use ScientificAxisLabelOptions. */
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
||||||
@@ -71,9 +72,11 @@ export function formatScientificAxisLabel(
|
|||||||
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
||||||
const formattedValue = formatYAxisNumber(scaledValue)
|
const formattedValue = formatYAxisNumber(scaledValue)
|
||||||
const topTickValue = options.topTickValue ?? options.axisMax
|
const topTickValue = options.topTickValue ?? options.axisMax
|
||||||
return exponent !== null && value === topTickValue
|
if (value !== topTickValue) return formattedValue
|
||||||
? `${formatExponent(exponent)} ${formattedValue}`
|
const unit = options.unit?.trim()
|
||||||
: formattedValue
|
const unitLabel = unit ? `(${unit}) ` : ''
|
||||||
|
const exponentLabel = exponent === null ? '' : `${formatExponent(exponent)} `
|
||||||
|
return `${exponentLabel}${unitLabel}${formattedValue}`
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Return the shared E-style multiplier for an axis, or null for a plain axis. */
|
/** Return the shared E-style multiplier for an axis, or null for a plain axis. */
|
||||||
|
|||||||
Reference in New Issue
Block a user