Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c5aa3c662a | ||
|
|
f98c264562 |
@@ -219,7 +219,7 @@ import 'waveform-analysis/style.css'
|
|||||||
|
|
||||||
组件支持 Plotly 风格的矩形框选缩放:在 zoom 模式下按住鼠标左键拖拽,松开后同时缩放
|
组件支持 Plotly 风格的矩形框选缩放:在 zoom 模式下按住鼠标左键拖拽,松开后同时缩放
|
||||||
X/Y 轴;设置 `pannable` 后,指针位于图表内时按住空格键拖拽可平移当前视口。
|
X/Y 轴;设置 `pannable` 后,指针位于图表内时按住空格键拖拽可平移当前视口。
|
||||||
鼠标滚轮仍可放大,双击恢复完整视口。
|
鼠标滚轮可放大和缩小,双击恢复完整视口。
|
||||||
组件会在滚轮或框选缩放结束后触发 `zoom-end`,调用方可以使用端点请求后端,再通过
|
组件会在滚轮或框选缩放结束后触发 `zoom-end`,调用方可以使用端点请求后端,再通过
|
||||||
`data` 传回新数据。独立分图模式还会包含 `trackIndex` 和稳定的 `seriesIds`。
|
`data` 传回新数据。独立分图模式还会包含 `trackIndex` 和稳定的 `seriesIds`。
|
||||||
|
|
||||||
@@ -603,7 +603,7 @@ async function importAnnotationFile(file: File) {
|
|||||||
字段无效时会抛出 `TypeError`,不会返回部分结果。导入包含未知 `seriesId` 的标注是允许的,
|
字段无效时会抛出 `TypeError`,不会返回部分结果。导入包含未知 `seriesId` 的标注是允许的,
|
||||||
对应曲线加载后会恢复显示。文件选择、错误提示和下载由业务层实现。
|
对应曲线加载后会恢复显示。文件选择、错误提示和下载由业务层实现。
|
||||||
|
|
||||||
X、Y 轴会根据各自完整显示域选择格式:最大绝对值在 `[0.01, 100)` 时显示两位普通小数;大于等于 `100`,或大于 `0` 且小于 `0.01` 时,刻度显示两位缩放值,并在轴末端单独显示共享倍率 `E±NN`。X 轴先按 `timeUnit` 转换为秒或毫秒再判断范围,多 Y 轴则分别计算倍率。tooltip 使用最多 4 位小数的本地化普通数字;标注编辑器的 X 坐标跟随 `timeUnit` 并固定 3 位小数,Y 坐标显示完整普通十进制。所有格式化都只发生在展示层,内部坐标值保持原始精度。
|
X 轴刻度和左右端点先按 `timeUnit` 转换为秒或毫秒,再四舍五入为不带分组符的普通整数,不使用科学计数法。Y 轴会根据完整显示域选择格式:最大绝对值在 `[0.01, 100)` 时显示两位普通小数;大于等于 `100`,或大于 `0` 且小于 `0.01` 时,刻度显示两位缩放值,并在轴末端单独显示共享倍率 `E±NN`,多 Y 轴分别计算倍率。tooltip 使用最多 4 位小数的本地化普通数字并省略无意义尾零;标注编辑器的 X 坐标跟随 `timeUnit` 并固定 3 位小数,Y 坐标显示完整普通十进制。所有格式化都只发生在展示层,内部坐标值保持原始精度。
|
||||||
标注框默认布局在采样点正上方,只做绘图区边界裁剪;文本框通过连接箭头指向标注位置,多个标注重叠时可通过拖动手动避让。
|
标注框默认布局在采样点正上方,只做绘图区边界裁剪;文本框通过连接箭头指向标注位置,多个标注重叠时可通过拖动手动避让。
|
||||||
|
|
||||||
## 事件
|
## 事件
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "waveform-analysis",
|
"name": "waveform-analysis",
|
||||||
"version": "0.1.26",
|
"version": "0.1.28",
|
||||||
"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",
|
||||||
|
|||||||
@@ -192,7 +192,7 @@ describe('multi-value Y-axis grouping', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('places left and right scientific exponents eight pixels outside their tick labels', () => {
|
it('reserves label clearance for exponent-prefixed ticks on both axis sides', () => {
|
||||||
const layout = buildTrackLayouts({
|
const layout = buildTrackLayouts({
|
||||||
cells: [
|
cells: [
|
||||||
{
|
{
|
||||||
@@ -206,7 +206,7 @@ describe('multi-value Y-axis grouping', () => {
|
|||||||
plotHeight: 300,
|
plotHeight: 300,
|
||||||
cellHeight: 330,
|
cellHeight: 330,
|
||||||
xAxisBand: 30,
|
xAxisBand: 30,
|
||||||
series: track([series('left', 0, 254), series('right', 0, 254)]),
|
series: track([series('left', 1000, 3000), series('right', 1000, 3000)]),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
||||||
@@ -222,14 +222,13 @@ describe('multi-value Y-axis grouping', () => {
|
|||||||
})[0]
|
})[0]
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
layout?.yAxes.map(({ side, x, exponentX, exponentLabel }) => ({
|
layout?.yAxes.map(({ side, x, labelX }) => ({
|
||||||
side,
|
side,
|
||||||
offset: Math.abs(exponentX - x),
|
labelOffset: Math.abs(labelX - x),
|
||||||
exponentLabel,
|
|
||||||
})),
|
})),
|
||||||
).toEqual([
|
).toEqual([
|
||||||
{ side: 'left', offset: 43, exponentLabel: 'E+02' },
|
{ side: 'left', labelOffset: 67 },
|
||||||
{ side: 'right', offset: 43, exponentLabel: 'E+02' },
|
{ side: 'right', labelOffset: 67 },
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -237,7 +236,7 @@ describe('multi-value Y-axis grouping', () => {
|
|||||||
const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
|
const [group] = buildYAxisSeriesGroups(track([series('long', -1e120, 1e120)]), 'multi-axis')
|
||||||
|
|
||||||
expect(group).toBeDefined()
|
expect(group).toBeDefined()
|
||||||
expect(measureYAxisGroupClearance(group!)).toBe(119)
|
expect(measureYAxisGroupClearance(group!)).toBe(90)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { scaleLinear } from 'd3'
|
import { scaleLinear } from 'd3'
|
||||||
|
|
||||||
import type { WaveformOverlayMode } from '../../types'
|
import type { WaveformOverlayMode } from '../../types'
|
||||||
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
|
import { formatScientificAxisLabel, paddedDomain } from '../../utils'
|
||||||
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
||||||
import { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
|
import { MAX_MULTI_Y_AXIS_COUNT } from './constants'
|
||||||
import {
|
import {
|
||||||
mergeYDomains,
|
mergeYDomains,
|
||||||
resolveSeriesFixedYDomain,
|
resolveSeriesFixedYDomain,
|
||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from './yDomain'
|
} from './yDomain'
|
||||||
|
|
||||||
// 导出常量供外部使用
|
// 导出常量供外部使用
|
||||||
export { MAX_MULTI_Y_AXIS_COUNT, Y_AXIS_EXPONENT_GAP } from './constants'
|
export { MAX_MULTI_Y_AXIS_COUNT } from './constants'
|
||||||
|
|
||||||
const Y_AXIS_CHARACTER_WIDTH = 7
|
const Y_AXIS_CHARACTER_WIDTH = 7
|
||||||
const Y_AXIS_TICK_PADDING = 7
|
const Y_AXIS_TICK_PADDING = 7
|
||||||
@@ -102,36 +102,27 @@ export function resolveYAxisSeriesGroups(
|
|||||||
export function axisTextMetrics(
|
export function axisTextMetrics(
|
||||||
domain: [number, number],
|
domain: [number, number],
|
||||||
nice = true,
|
nice = true,
|
||||||
): {
|
tickValues?: number[],
|
||||||
exponentLabel: string | null
|
): { tickTextWidth: number } {
|
||||||
exponentWidth: number
|
|
||||||
tickTextWidth: number
|
|
||||||
} {
|
|
||||||
const scale = scaleLinear(domain, [1, 0])
|
const scale = scaleLinear(domain, [1, 0])
|
||||||
if (nice) scale.nice()
|
if (nice) scale.nice()
|
||||||
const [axisMin, axisMax] = scale.domain()
|
const [axisMin, axisMax] = scale.domain()
|
||||||
const values = scale.ticks(10)
|
const values = tickValues ?? Array.from(new Set([axisMin, ...scale.ticks(10), axisMax]))
|
||||||
|
const topTickValue = Math.max(...values)
|
||||||
const maximumTickCharacters = Math.max(
|
const maximumTickCharacters = Math.max(
|
||||||
1,
|
1,
|
||||||
...values.map((value) => formatScientificAxisLabel(value, { axisMin, axisMax }).length),
|
...values.map(
|
||||||
|
(value) => formatScientificAxisLabel(value, { axisMin, axisMax, topTickValue }).length,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
const exponentLabel = formatScientificAxisExponent(axisMin, axisMax)
|
|
||||||
return {
|
return {
|
||||||
exponentLabel,
|
|
||||||
exponentWidth: exponentLabel ? exponentLabel.length * Y_AXIS_CHARACTER_WIDTH : 0,
|
|
||||||
tickTextWidth: maximumTickCharacters * Y_AXIS_CHARACTER_WIDTH,
|
tickTextWidth: maximumTickCharacters * Y_AXIS_CHARACTER_WIDTH,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function axisExponentClearance(domain: [number, number], nice: boolean): number {
|
|
||||||
const { exponentLabel, exponentWidth } = axisTextMetrics(domain, nice)
|
|
||||||
return exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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).tickTextWidth +
|
||||||
axisExponentClearance(group.domain, !group.fixed) +
|
|
||||||
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 +
|
||||||
@@ -142,7 +133,6 @@ 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).tickTextWidth +
|
||||||
axisExponentClearance(group.domain, !group.fixed) +
|
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
Y_AXIS_OUTER_PADDING
|
Y_AXIS_OUTER_PADDING
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
type ResolvedWaveformRenderingOptions,
|
type ResolvedWaveformRenderingOptions,
|
||||||
} from '../../core/rendering'
|
} from '../../core/rendering'
|
||||||
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
|
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
|
||||||
import { buildMinorTicks, formatAxisTimeExponent, formatEndpointTime } from '../../utils'
|
import { buildMinorTicks, formatEndpointTime } from '../../utils'
|
||||||
import {
|
import {
|
||||||
getBottomRowCellIndexes,
|
getBottomRowCellIndexes,
|
||||||
type GridCellGeometry,
|
type GridCellGeometry,
|
||||||
@@ -21,7 +21,6 @@ import {
|
|||||||
} from './grid'
|
} from './grid'
|
||||||
import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
|
import { axisTextMetrics, resolveYAxisSeriesGroups } from './layout'
|
||||||
import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
import type { DisplaySeries, DisplayTrack, TrackLayout, WaveformYAxisLayout } from './types'
|
||||||
import { Y_AXIS_EXPONENT_GAP } from './constants'
|
|
||||||
import {
|
import {
|
||||||
Y_AXIS_LABEL_BAND_WIDTH,
|
Y_AXIS_LABEL_BAND_WIDTH,
|
||||||
Y_AXIS_LABEL_GAP,
|
Y_AXIS_LABEL_GAP,
|
||||||
@@ -86,6 +85,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
? scaleLinear(
|
? scaleLinear(
|
||||||
options.initialXDomains?.[displayTrack.id] ??
|
options.initialXDomains?.[displayTrack.id] ??
|
||||||
options.initialXDomains?.[series.id] ??
|
options.initialXDomains?.[series.id] ??
|
||||||
|
options.initialXDomain ??
|
||||||
displayTrack.xDomain,
|
displayTrack.xDomain,
|
||||||
[0, cell.width],
|
[0, cell.width],
|
||||||
)
|
)
|
||||||
@@ -117,31 +117,16 @@ 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 { exponentLabel, exponentWidth, tickTextWidth } = axisTextMetrics(
|
const { tickTextWidth } = axisTextMetrics(group.domain, !group.fixed, tickValues)
|
||||||
group.domain,
|
|
||||||
!group.fixed,
|
|
||||||
)
|
|
||||||
const exponentClearance = exponentLabel ? exponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
||||||
const clearance =
|
const clearance =
|
||||||
tickTextWidth +
|
tickTextWidth +
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
exponentClearance +
|
|
||||||
Y_AXIS_LABEL_GAP +
|
Y_AXIS_LABEL_GAP +
|
||||||
Y_AXIS_LABEL_BAND_WIDTH +
|
Y_AXIS_LABEL_BAND_WIDTH +
|
||||||
Y_AXIS_OUTER_PADDING
|
Y_AXIS_OUTER_PADDING
|
||||||
const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
|
const x = group.side === 'left' ? -sideOffsets.left : cell.width + sideOffsets.right
|
||||||
const exponentX =
|
|
||||||
x +
|
|
||||||
(group.side === 'left'
|
|
||||||
? -(Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
||||||
: Y_AXIS_TICK_PADDING + tickTextWidth + Y_AXIS_EXPONENT_GAP)
|
|
||||||
const labelDistance =
|
const labelDistance =
|
||||||
tickTextWidth +
|
tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_LABEL_GAP + Y_AXIS_LABEL_BAND_WIDTH / 2
|
||||||
Y_AXIS_TICK_PADDING +
|
|
||||||
exponentClearance +
|
|
||||||
exponentWidth +
|
|
||||||
Y_AXIS_LABEL_GAP +
|
|
||||||
Y_AXIS_LABEL_BAND_WIDTH / 2
|
|
||||||
const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
|
const labelX = x + (group.side === 'left' ? -labelDistance : labelDistance)
|
||||||
sideOffsets[group.side] += clearance
|
sideOffsets[group.side] += clearance
|
||||||
return {
|
return {
|
||||||
@@ -149,8 +134,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
side: group.side,
|
side: group.side,
|
||||||
x,
|
x,
|
||||||
labelX,
|
labelX,
|
||||||
exponentX,
|
|
||||||
exponentLabel,
|
|
||||||
scale,
|
scale,
|
||||||
majorTicks,
|
majorTicks,
|
||||||
minorTicks: buildMinorTicks(majorTicks),
|
minorTicks: buildMinorTicks(majorTicks),
|
||||||
@@ -167,7 +150,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
start: formatEndpointTime(domain[0], domain, options.timeUnit),
|
start: formatEndpointTime(domain[0], domain, options.timeUnit),
|
||||||
end: formatEndpointTime(domain[1], domain, options.timeUnit),
|
end: formatEndpointTime(domain[1], domain, options.timeUnit),
|
||||||
}
|
}
|
||||||
const xAxisExponent = formatAxisTimeExponent(domain, options.timeUnit)
|
|
||||||
const leftClearance = endpointLabels.start.length * 7 + 10
|
const leftClearance = endpointLabels.start.length * 7 + 10
|
||||||
const rightClearance = endpointLabels.end.length * 7 + 10
|
const rightClearance = endpointLabels.end.length * 7 + 10
|
||||||
const xAxisTickValues = xMajorTicks.filter((tick) => {
|
const xAxisTickValues = xMajorTicks.filter((tick) => {
|
||||||
@@ -234,7 +216,6 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
|||||||
yAxisTickValues,
|
yAxisTickValues,
|
||||||
xAxisTickValues,
|
xAxisTickValues,
|
||||||
endpointLabels,
|
endpointLabels,
|
||||||
xAxisExponent,
|
|
||||||
path: seriesPaths[0]?.path ?? null,
|
path: seriesPaths[0]?.path ?? null,
|
||||||
seriesPaths,
|
seriesPaths,
|
||||||
gridLines: options.grid.trackLines[displayTrack.id] ?? {
|
gridLines: options.grid.trackLines[displayTrack.id] ?? {
|
||||||
|
|||||||
@@ -51,8 +51,6 @@ export interface WaveformYAxisLayout {
|
|||||||
side: 'left' | 'right'
|
side: 'left' | 'right'
|
||||||
x: number
|
x: number
|
||||||
labelX: number
|
labelX: number
|
||||||
exponentX: number
|
|
||||||
exponentLabel: string | null
|
|
||||||
scale: ScaleLinear<number, number>
|
scale: ScaleLinear<number, number>
|
||||||
majorTicks: number[]
|
majorTicks: number[]
|
||||||
minorTicks: number[]
|
minorTicks: number[]
|
||||||
@@ -109,7 +107,6 @@ export interface TrackLayout {
|
|||||||
yAxisTickValues: number[]
|
yAxisTickValues: number[]
|
||||||
xAxisTickValues: number[]
|
xAxisTickValues: number[]
|
||||||
endpointLabels: { start: string; end: string }
|
endpointLabels: { start: string; end: string }
|
||||||
xAxisExponent: string | null
|
|
||||||
path: string | null
|
path: string | null
|
||||||
seriesPaths: TrackSeriesPath[]
|
seriesPaths: TrackSeriesPath[]
|
||||||
showXAxis: boolean
|
showXAxis: boolean
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { scaleLinear, type ZoomTransform } from 'd3'
|
|||||||
import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
||||||
|
|
||||||
import { resolveWaveformRenderingOptions } from '../../core'
|
import { resolveWaveformRenderingOptions } from '../../core'
|
||||||
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
|
import { paddedDomain } from '../../utils'
|
||||||
import {
|
import {
|
||||||
layoutAnnotations,
|
layoutAnnotations,
|
||||||
type AnnotationSeriesInfo,
|
type AnnotationSeriesInfo,
|
||||||
@@ -28,9 +28,9 @@ import {
|
|||||||
} from './grid'
|
} from './grid'
|
||||||
import {
|
import {
|
||||||
buildTrackLayouts,
|
buildTrackLayouts,
|
||||||
|
axisTextMetrics,
|
||||||
measureTrackYAxisClearance,
|
measureTrackYAxisClearance,
|
||||||
resolveYAxisSeriesGroups,
|
resolveYAxisSeriesGroups,
|
||||||
Y_AXIS_EXPONENT_GAP,
|
|
||||||
} from './layout'
|
} from './layout'
|
||||||
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
||||||
import type { PreparedWaveformSeries } from './useWaveformData'
|
import type { PreparedWaveformSeries } from './useWaveformData'
|
||||||
@@ -112,40 +112,18 @@ 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) => {
|
.map((group) => axisTextMetrics(group.domain, !group.fixed).tickTextWidth)
|
||||||
const scale = scaleLinear(group.domain, [1, 0])
|
const tickTextWidth = Math.max(Y_AXIS_CHARACTER_WIDTH, ...axisText)
|
||||||
if (!group.fixed) scale.nice()
|
const tickClearance = tickTextWidth + Y_AXIS_TICK_PADDING + Y_AXIS_OUTER_PADDING
|
||||||
const [axisMin, axisMax] = scale.domain()
|
|
||||||
return {
|
|
||||||
exponentLabel: formatScientificAxisExponent(axisMin, axisMax),
|
|
||||||
tickLabels: scale
|
|
||||||
.ticks(10)
|
|
||||||
.map((value) => formatScientificAxisLabel(value, { axisMin, axisMax })),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
const maximumCharacters = Math.max(
|
|
||||||
1,
|
|
||||||
...axisText.flatMap(({ tickLabels }) => tickLabels).map((label) => label.length),
|
|
||||||
)
|
|
||||||
const tickTextWidth = maximumCharacters * Y_AXIS_CHARACTER_WIDTH
|
|
||||||
const maximumExponentWidth = Math.max(
|
|
||||||
0,
|
|
||||||
...axisText.map(({ exponentLabel }) => (exponentLabel?.length ?? 0) * Y_AXIS_CHARACTER_WIDTH),
|
|
||||||
)
|
|
||||||
const exponentClearance = maximumExponentWidth ? maximumExponentWidth + Y_AXIS_EXPONENT_GAP : 0
|
|
||||||
const tickClearance =
|
|
||||||
tickTextWidth + Y_AXIS_TICK_PADDING + exponentClearance + Y_AXIS_OUTER_PADDING
|
|
||||||
const labelCenterX = -(
|
const labelCenterX = -(
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
tickTextWidth +
|
tickTextWidth +
|
||||||
exponentClearance +
|
|
||||||
Y_AXIS_LABEL_GAP +
|
Y_AXIS_LABEL_GAP +
|
||||||
Y_AXIS_LABEL_BAND_WIDTH / 2
|
Y_AXIS_LABEL_BAND_WIDTH / 2
|
||||||
)
|
)
|
||||||
const fullClearance =
|
const fullClearance =
|
||||||
tickTextWidth +
|
tickTextWidth +
|
||||||
Y_AXIS_TICK_PADDING +
|
Y_AXIS_TICK_PADDING +
|
||||||
exponentClearance +
|
|
||||||
Y_AXIS_LABEL_GAP +
|
Y_AXIS_LABEL_GAP +
|
||||||
Y_AXIS_LABEL_BAND_WIDTH +
|
Y_AXIS_LABEL_BAND_WIDTH +
|
||||||
Y_AXIS_OUTER_PADDING
|
Y_AXIS_OUTER_PADDING
|
||||||
|
|||||||
@@ -91,6 +91,22 @@ describe('WaveformTooltip', () => {
|
|||||||
expect(wrapper.get('.waveform-tooltip__value').text()).toBe('-1,405.4932 A')
|
expect(wrapper.get('.waveform-tooltip__value').text()).toBe('-1,405.4932 A')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('formats tooltip time with at most four decimal places', () => {
|
||||||
|
const wrapper = mount(WaveformTooltip, {
|
||||||
|
props: {
|
||||||
|
visible: true,
|
||||||
|
position: { x: 10, y: 10 },
|
||||||
|
timeUnit: 's',
|
||||||
|
hoveredPoint: { x: 1.234567, y: 12 },
|
||||||
|
seriesPoints: [],
|
||||||
|
containerWidth: 400,
|
||||||
|
containerHeight: 300,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(wrapper.get('.waveform-tooltip__time').text()).toBe('s: 1.2346')
|
||||||
|
})
|
||||||
|
|
||||||
it('omits the error label when both resolved errors are zero', () => {
|
it('omits the error label when both resolved errors are zero', () => {
|
||||||
const point = { x: 1, y: 12 }
|
const point = { x: 1, y: 12 }
|
||||||
const wrapper = mount(WaveformTooltip, {
|
const wrapper = mount(WaveformTooltip, {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
select,
|
select,
|
||||||
zoom,
|
zoom,
|
||||||
zoomIdentity,
|
zoomIdentity,
|
||||||
|
zoomTransform,
|
||||||
type D3ZoomEvent,
|
type D3ZoomEvent,
|
||||||
type ZoomBehavior,
|
type ZoomBehavior,
|
||||||
type ZoomTransform,
|
type ZoomTransform,
|
||||||
@@ -237,6 +238,15 @@ export function useWaveformZoom(context: ZoomContext) {
|
|||||||
const tracks = trackLayouts.value.filter((track) => track.hasVisibleSeries)
|
const tracks = trackLayouts.value.filter((track) => track.hasVisibleSeries)
|
||||||
return tracks.length > 0 && tracks.every(canZoomTrack)
|
return tracks.length > 0 && tracks.every(canZoomTrack)
|
||||||
}
|
}
|
||||||
|
const canHandleWheelZoom = (event: Event, canZoomIn: boolean): boolean => {
|
||||||
|
if (event.type !== 'wheel') return false
|
||||||
|
const deltaY = (event as WheelEvent).deltaY
|
||||||
|
if (deltaY < 0) return canZoomIn
|
||||||
|
if (deltaY > 0) {
|
||||||
|
return zoomTransform(event.currentTarget as Element).k > ZOOM_CONSTRAINTS.MIN_SCALE
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
const configureZoom = () => {
|
const configureZoom = () => {
|
||||||
clearZoomBindings()
|
clearZoomBindings()
|
||||||
if (
|
if (
|
||||||
@@ -256,10 +266,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|||||||
if (!overlay) return
|
if (!overlay) return
|
||||||
const dataDomain = resolveInitialTrackDomain(track)
|
const dataDomain = resolveInitialTrackDomain(track)
|
||||||
const behavior = zoom<SVGRectElement, unknown>()
|
const behavior = zoom<SVGRectElement, unknown>()
|
||||||
.filter(
|
.filter((event) => canHandleWheelZoom(event, canZoomTrack(track)))
|
||||||
(event) =>
|
|
||||||
event.type === 'wheel' && (event as WheelEvent).deltaY < 0 && canZoomTrack(track),
|
|
||||||
)
|
|
||||||
.scaleExtent([1, resolveMaximumZoomScale(dataDomain)])
|
.scaleExtent([1, resolveMaximumZoomScale(dataDomain)])
|
||||||
.extent([
|
.extent([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
@@ -287,10 +294,7 @@ export function useWaveformZoom(context: ZoomContext) {
|
|||||||
const overlay = sharedOverlayElement.value
|
const overlay = sharedOverlayElement.value
|
||||||
if (!overlay) return
|
if (!overlay) return
|
||||||
const behavior = zoom<SVGRectElement, unknown>()
|
const behavior = zoom<SVGRectElement, unknown>()
|
||||||
.filter(
|
.filter((event) => canHandleWheelZoom(event, canZoomSharedTracks()))
|
||||||
(event) =>
|
|
||||||
event.type === 'wheel' && (event as WheelEvent).deltaY < 0 && canZoomSharedTracks(),
|
|
||||||
)
|
|
||||||
.scaleExtent([1, resolveMaximumZoomScale(initialXDomain.value)])
|
.scaleExtent([1, resolveMaximumZoomScale(initialXDomain.value)])
|
||||||
.extent([
|
.extent([
|
||||||
[0, 0],
|
[0, 0],
|
||||||
|
|||||||
@@ -43,8 +43,11 @@ function renderAxes() {
|
|||||||
const element = yAxisElements.value[index]
|
const element = yAxisElements.value[index]
|
||||||
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 yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
const yAxis = (axis.side === 'left' ? axisLeft(axis.scale) : axisRight(axis.scale))
|
||||||
.tickFormat((value) => formatScientificAxisLabel(Number(value), { axisMin, axisMax }))
|
.tickFormat((value) =>
|
||||||
|
formatScientificAxisLabel(Number(value), { axisMin, axisMax, topTickValue }),
|
||||||
|
)
|
||||||
.tickSize(-4)
|
.tickSize(-4)
|
||||||
.tickPadding(7)
|
.tickPadding(7)
|
||||||
.tickSizeOuter(0)
|
.tickSizeOuter(0)
|
||||||
@@ -136,17 +139,6 @@ watch(
|
|||||||
{{ track.endpointLabels.end }}
|
{{ track.endpointLabels.end }}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
<text
|
|
||||||
v-if="track.showXAxis && track.xAxisExponent"
|
|
||||||
class="waveform-track__axis-exponent waveform-track__axis-exponent--x waveform-chart__axis-exponent waveform-chart__axis-exponent--x"
|
|
||||||
:x="track.width ?? innerWidth"
|
|
||||||
:y="track.height + 27"
|
|
||||||
text-anchor="end"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{{ track.xAxisExponent }}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<g
|
<g
|
||||||
v-for="axis in track.isEmpty ? [] : track.yAxes"
|
v-for="axis in track.isEmpty ? [] : track.yAxes"
|
||||||
:key="`y-axis-${track.index}-${axis.index}`"
|
:key="`y-axis-${track.index}-${axis.index}`"
|
||||||
@@ -160,20 +152,6 @@ watch(
|
|||||||
:data-y-axis-side="axis.side"
|
:data-y-axis-side="axis.side"
|
||||||
:transform="`translate(${axis.x}, 0)`"
|
:transform="`translate(${axis.x}, 0)`"
|
||||||
/>
|
/>
|
||||||
<text
|
|
||||||
v-for="axis in track.isEmpty ? [] : track.yAxes.filter((item) => item.exponentLabel)"
|
|
||||||
:key="`y-axis-exponent-${track.index}-${axis.index}`"
|
|
||||||
class="waveform-track__axis-exponent waveform-track__axis-exponent--y waveform-chart__axis-exponent waveform-chart__axis-exponent--y"
|
|
||||||
:data-y-axis-index="axis.index"
|
|
||||||
:x="axis.exponentX"
|
|
||||||
y="0"
|
|
||||||
dy="0.32em"
|
|
||||||
:text-anchor="axis.side === 'left' ? 'end' : 'start'"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
{{ axis.exponentLabel }}
|
|
||||||
</text>
|
|
||||||
|
|
||||||
<g
|
<g
|
||||||
v-if="
|
v-if="
|
||||||
!cleanView &&
|
!cleanView &&
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('WaveformChart', () => {
|
|||||||
expect(endTicks[3]).toHaveLength(0)
|
expect(endTicks[3]).toHaveLength(0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps one separate shared exponent for every compact Y axis', async () => {
|
it('prefixes one shared exponent to the largest visible tick on every compact Y axis', async () => {
|
||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
kind: 'series',
|
kind: 'series',
|
||||||
@@ -83,7 +83,7 @@ describe('WaveformChart', () => {
|
|||||||
kind: 'points',
|
kind: 'points',
|
||||||
points: [
|
points: [
|
||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ x: 1, y: 254 },
|
{ x: 1, y: 2540 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -103,14 +103,14 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const axes = wrapper.findAll('.waveform-chart__axis--y')
|
const axes = wrapper.findAll('.waveform-chart__axis--y')
|
||||||
const exponents = wrapper.findAll('.waveform-chart__axis-exponent--y')
|
|
||||||
expect(axes).toHaveLength(2)
|
expect(axes).toHaveLength(2)
|
||||||
expect(exponents.map((label) => label.text())).toEqual(['E+02', 'E-04'])
|
axes.forEach((axis, index) => {
|
||||||
axes.forEach((axis) => {
|
|
||||||
const labels = axis.findAll('.tick text').map((tick) => tick.text())
|
const labels = axis.findAll('.tick text').map((tick) => tick.text())
|
||||||
expect(labels.every((label) => !label.startsWith('E'))).toBe(true)
|
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
||||||
expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
|
expect(exponentLabels).toHaveLength(1)
|
||||||
|
expect(exponentLabels[0]).toMatch(index === 0 ? /^E\+03 / : /^E-04 /)
|
||||||
})
|
})
|
||||||
|
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('prefers a trimmed series name and falls back to yLabel for unnamed data', async () => {
|
it('prefers a trimmed series name and falls back to yLabel for unnamed data', async () => {
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ describe('WaveformChart', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
expect(wrapper.emitted('point-hover')?.at(-1)).toEqual([{ x: 1, y: 5 }])
|
||||||
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
expect(wrapper.find('.waveform-chart__tooltip').exists()).toBe(true)
|
||||||
expect(wrapper.get('.waveform-chart__tooltip').text()).toContain('ms: 1,000.0000')
|
expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
|
||||||
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
const crosshairLines = wrapper.findAll('.waveform-chart__crosshair line')
|
||||||
expect(crosshairLines).toHaveLength(1)
|
expect(crosshairLines).toHaveLength(1)
|
||||||
expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
|
expect(crosshairLines[0].attributes('x1')).toBe(crosshairLines[0].attributes('x2'))
|
||||||
@@ -303,7 +303,7 @@ describe('WaveformChart', () => {
|
|||||||
flushAnimationFrames()
|
flushAnimationFrames()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.get('.waveform-chart__tooltip').text()).toContain('ms: 1,000.0000')
|
expect(wrapper.get('.waveform-chart__tooltip-time').text()).toBe('ms: 1,000')
|
||||||
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
|
expect(wrapper.findAll('.waveform-chart__crosshair line')).toHaveLength(1)
|
||||||
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
|
expect(wrapper.get('.waveform-chart__line').element).toBe(pathBeforeHover)
|
||||||
expect(chartUpdate).not.toHaveBeenCalled()
|
expect(chartUpdate).not.toHaveBeenCalled()
|
||||||
|
|||||||
@@ -62,15 +62,15 @@ describe('WaveformChart', () => {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
const wrapper = await mountSizedChart(firstData)
|
const wrapper = await mountSizedChart(firstData)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||||
|
|
||||||
firstData.points.push({ x: 2, y: 2 })
|
firstData.points.push({ x: 2, y: 2 })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||||
|
|
||||||
await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
|
await wrapper.setProps({ data: { ...firstData, points: [...firstData.points] } })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps controlled annotations when replacing the loaded data window', async () => {
|
it('keeps controlled annotations when replacing the loaded data window', async () => {
|
||||||
@@ -170,7 +170,7 @@ describe('WaveformChart', () => {
|
|||||||
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
expect(wrapper.findAll('.waveform-chart__track-label')).toHaveLength(0)
|
||||||
expect(
|
expect(
|
||||||
wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
|
wrapper.findAll('.waveform-chart__axis-endpoint--end').map((item) => item.text()),
|
||||||
).toEqual(['1.00', '2.00'])
|
).toEqual(['1000', '2000'])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the zero Y-axis label on upper compact tracks', async () => {
|
it('keeps the zero Y-axis label on upper compact tracks', async () => {
|
||||||
|
|||||||
@@ -162,35 +162,42 @@ describe('WaveformChart', () => {
|
|||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').attributes('x')).toBe(
|
||||||
String(trackWidth),
|
String(trackWidth),
|
||||||
)
|
)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4.99')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('4990')
|
||||||
expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
|
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
|
it('uses one shared scientific exponent only for large and tiny Y-axis domains', async () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
{ values: [0, 50], exponent: null },
|
{ values: [0, 50], yDomain: [0, 50] as [number, number], exponent: null },
|
||||||
{ values: [0, 254], exponent: 'E+02' },
|
{ values: [1000, 3000], yDomain: [1000, 3000] as [number, number], exponent: 'E+03' },
|
||||||
{ values: [0, 0.0002], exponent: 'E-04' },
|
{
|
||||||
|
values: [0.0001, 0.0003],
|
||||||
|
yDomain: [0.0001, 0.0003] as [number, number],
|
||||||
|
exponent: 'E-04',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const { values, exponent } of cases) {
|
for (const { values, yDomain, exponent } of cases) {
|
||||||
const wrapper = await mountSizedChart({
|
const data = {
|
||||||
kind: 'points',
|
kind: 'points' as const,
|
||||||
points: values.map((y, x) => ({ x, y })),
|
points: values.map((y, x) => ({ x, y })),
|
||||||
})
|
}
|
||||||
|
const originalValues = data.points.map((point) => point.y)
|
||||||
|
const wrapper = await mountSizedChart(data, { yDomain })
|
||||||
const labels = wrapper
|
const labels = wrapper
|
||||||
.get('.waveform-chart__axis--y')
|
.get('.waveform-chart__axis--y')
|
||||||
.findAll('.tick text')
|
.findAll('.tick text')
|
||||||
.map((tick) => tick.text())
|
.map((tick) => tick.text())
|
||||||
const exponentLabel = wrapper.find('.waveform-chart__axis-exponent--y')
|
const exponentLabels = labels.filter((label) => label.startsWith('E'))
|
||||||
|
|
||||||
if (exponent === null) {
|
if (exponent === null) {
|
||||||
expect(exponentLabel.exists()).toBe(false)
|
expect(exponentLabels).toHaveLength(0)
|
||||||
} else {
|
} else {
|
||||||
expect(exponentLabel.text()).toBe(exponent)
|
expect(exponentLabels).toHaveLength(1)
|
||||||
expect(labels.every((label) => !label.startsWith('E'))).toBe(true)
|
expect(exponentLabels[0]).toMatch(new RegExp(`^${exponent.replace('+', '\\+')} `))
|
||||||
expect(labels.every((label) => /^-?\d+\.\d{2}$/.test(label))).toBe(true)
|
|
||||||
}
|
}
|
||||||
|
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
||||||
|
expect(data.points.map((point) => point.y)).toEqual(originalValues)
|
||||||
|
|
||||||
wrapper.unmount()
|
wrapper.unmount()
|
||||||
}
|
}
|
||||||
@@ -212,13 +219,13 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
expect(millisecondsChart.text()).toContain('时间(ms)')
|
expect(millisecondsChart.text()).toContain('时间(ms)')
|
||||||
expect(millisecondTicks.length).toBeGreaterThan(0)
|
expect(millisecondTicks.length).toBeGreaterThan(0)
|
||||||
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
expect(millisecondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||||
expect(millisecondsChart.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
|
expect(millisecondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
||||||
expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
|
expect(millisecondsChart.find('.waveform-chart__watermark').exists()).toBe(false)
|
||||||
|
|
||||||
const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
|
const secondsChart = await mountSizedChart(data, { timeUnit: 's', xLabel: 'Elapsed time' })
|
||||||
expect(secondsChart.text()).toContain('Elapsed time')
|
expect(secondsChart.text()).toContain('Elapsed time')
|
||||||
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
expect(secondsChart.get('.waveform-chart__axis-endpoint--end').text()).toBe('1')
|
||||||
expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
expect(secondsChart.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -237,15 +244,15 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
expect(start.attributes('x')).toBe('0')
|
expect(start.attributes('x')).toBe('0')
|
||||||
expect(start.attributes('text-anchor')).toBe('start')
|
expect(start.attributes('text-anchor')).toBe('start')
|
||||||
expect(start.text()).toBe('0.00')
|
expect(start.text()).toBe('0')
|
||||||
expect(end.attributes('x')).toBe(
|
expect(end.attributes('x')).toBe(
|
||||||
wrapper.get('.waveform-chart__track').attributes('data-track-width'),
|
wrapper.get('.waveform-chart__track').attributes('data-track-width'),
|
||||||
)
|
)
|
||||||
expect(end.attributes('text-anchor')).toBe('end')
|
expect(end.attributes('text-anchor')).toBe('end')
|
||||||
expect(end.text()).toBe('2.00')
|
expect(end.text()).toBe('1999')
|
||||||
expect(middleTickLabels.length).toBeGreaterThan(0)
|
expect(middleTickLabels.length).toBeGreaterThan(0)
|
||||||
expect(middleTickLabels).not.toContain('0.00')
|
expect(middleTickLabels).not.toContain('0')
|
||||||
expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).toBe('E+03')
|
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
||||||
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
|
expect(wrapper.findAll('.waveform-chart__grid--major line').length).toBeGreaterThan(
|
||||||
middleTickLabels.length,
|
middleTickLabels.length,
|
||||||
)
|
)
|
||||||
@@ -300,6 +307,6 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe(initialStart)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).not.toBe(initialEnd)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toContain('.')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toMatch(/^-?\d+$/)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ describe('WaveformChart', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
|
expect(wrapper.findAll('.waveform-chart__axis--x')).toHaveLength(1)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('updates rendering props and disables zoom interaction', async () => {
|
it('updates rendering props and disables zoom interaction', async () => {
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ describe('WaveformChart', () => {
|
|||||||
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
tracks[0].get('.waveform-chart__y-axis-label-bg').attributes('x'),
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(labelX).toBe(-103)
|
expect(labelX).toBe(-74)
|
||||||
expect(labelBackgroundX).toBe(labelX - 12)
|
expect(labelBackgroundX).toBe(labelX - 12)
|
||||||
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(119)
|
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(90)
|
||||||
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(119)
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(90)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
it('keeps a tick-only gutter when channel labels are empty', async () => {
|
||||||
@@ -62,8 +62,8 @@ describe('WaveformChart', () => {
|
|||||||
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
||||||
|
|
||||||
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
expect(wrapper.findAll('.waveform-chart__y-axis-label')).toHaveLength(0)
|
||||||
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(89)
|
expect(Number(wrapper.attributes('data-chart-left-margin'))).toBe(64)
|
||||||
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(89)
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(60)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
|
it('keeps the Y-axis label gutter stable while paging between value ranges', async () => {
|
||||||
@@ -122,7 +122,7 @@ describe('WaveformChart', () => {
|
|||||||
const tracks = wrapper.findAll('.waveform-chart__track')
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
||||||
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
const firstWidth = Number(tracks[0].attributes('data-track-width'))
|
||||||
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
const secondLeft = Number(tracks[1].attributes('data-track-left'))
|
||||||
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(39)
|
expect(secondLeft - firstWidth).toBeGreaterThanOrEqual(32)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
|
it('uses one shared overlay and bottom-row x axes for separated and compact grids', async () => {
|
||||||
@@ -225,7 +225,7 @@ describe('WaveformChart', () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses each track x domain in independent mode instead of the shared initial domain', async () => {
|
it('uses the shared initial x domain for every independent track by default', async () => {
|
||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
kind: 'series',
|
kind: 'series',
|
||||||
@@ -262,8 +262,10 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const tracks = wrapper.findAll('.waveform-chart__track')
|
const tracks = wrapper.findAll('.waveform-chart__track')
|
||||||
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('6.00')
|
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
||||||
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5.00')
|
expect(tracks[0]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
||||||
|
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--start').text()).toBe('-8000')
|
||||||
|
expect(tracks[1]?.get('.waveform-chart__axis-endpoint--end').text()).toBe('5000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('uses an explicit initial x domain override for an independent track', async () => {
|
it('uses an explicit initial x domain override for an independent track', async () => {
|
||||||
@@ -291,7 +293,7 @@ describe('WaveformChart', () => {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('1000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
|
it('reacts to exact fixed Y-domain props and returns to automatic bounds', async () => {
|
||||||
@@ -310,13 +312,13 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
await wrapper.setProps({ yDomain: [3, 97] })
|
await wrapper.setProps({ yDomain: [3, 97] })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(yTickLabels()).toContain('3.00')
|
expect(yTickLabels()).toContain('3')
|
||||||
expect(yTickLabels()).toContain('97.00')
|
expect(yTickLabels()).toContain('97')
|
||||||
|
|
||||||
await wrapper.setProps({ yDomain: undefined })
|
await wrapper.setProps({ yDomain: undefined })
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(yTickLabels()).not.toContain('3.00')
|
expect(yTickLabels()).not.toContain('3')
|
||||||
expect(yTickLabels()).not.toContain('97.00')
|
expect(yTickLabels()).not.toContain('97')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps annotations bound to their channel while paging', async () => {
|
it('keeps annotations bound to their channel while paging', async () => {
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe('WaveformChart', () => {
|
|||||||
kind: 'points',
|
kind: 'points',
|
||||||
points: [
|
points: [
|
||||||
{ x: 0, y: 0 },
|
{ x: 0, y: 0 },
|
||||||
{ x: 1, y: 254 },
|
{ x: 1, y: 2540 },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -51,13 +51,13 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
wrapper
|
wrapper.findAll('.waveform-chart__axis--y').map((axis) =>
|
||||||
.findAll('.waveform-chart__axis-exponent--y')
|
axis
|
||||||
.map((label) => [label.attributes('data-y-axis-index'), label.text()]),
|
.findAll('.tick text')
|
||||||
).toEqual([
|
.map((label) => label.text())
|
||||||
['1', 'E+02'],
|
.filter((label) => label.startsWith('E')),
|
||||||
['2', 'E-04'],
|
),
|
||||||
])
|
).toEqual([[], [expect.stringMatching(/^E\+03 /)], [expect.stringMatching(/^E-04 /)]])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
it('reprojects annotations with the Y axis assigned to their series', async () => {
|
||||||
|
|||||||
@@ -54,8 +54,9 @@ describe('WaveformChart', () => {
|
|||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(first.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
expect(first.get('.waveform-chart__axis--y').text()).not.toContain('E')
|
||||||
expect(second.get('.waveform-chart__axis-exponent--y').text()).toBe('E+04')
|
expect(second.get('.waveform-chart__axis--y').text()).toContain('E+04 ')
|
||||||
|
expect(second.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('renders a configurable zero line only when the Y domain contains zero', async () => {
|
it('renders a configurable zero line only when the Y domain contains zero', async () => {
|
||||||
@@ -299,8 +300,9 @@ describe('WaveformChart', () => {
|
|||||||
})
|
})
|
||||||
expect(xAxis.get('path.domain').attributes('display')).toBe('none')
|
expect(xAxis.get('path.domain').attributes('display')).toBe('none')
|
||||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint')).toHaveLength(2)
|
||||||
expect(wrapper.get('.waveform-chart__axis-exponent--x').text()).not.toBe('')
|
expect(wrapper.find('.waveform-chart__axis-exponent--x').exists()).toBe(false)
|
||||||
expect(wrapper.get('.waveform-chart__axis-exponent--y').text()).not.toBe('')
|
expect(yAxes.some((axis) => axis.text().includes('E+04 '))).toBe(true)
|
||||||
|
expect(wrapper.find('.waveform-chart__axis-exponent--y').exists()).toBe(false)
|
||||||
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
|
expect(wrapper.find('.waveform-chart__grid').exists()).toBe(false)
|
||||||
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
expect(wrapper.get('.waveform-chart__plot-frame').attributes()).toMatchObject({
|
||||||
stroke: '#dc2626',
|
stroke: '#dc2626',
|
||||||
|
|||||||
@@ -241,7 +241,7 @@ describe('WaveformChart presentation mode', () => {
|
|||||||
await flushPromises()
|
await flushPromises()
|
||||||
overlay.element.dispatchEvent(
|
overlay.element.dispatchEvent(
|
||||||
new WheelEvent('wheel', {
|
new WheelEvent('wheel', {
|
||||||
deltaY: -200,
|
deltaY: 200,
|
||||||
clientX: width / 2,
|
clientX: width / 2,
|
||||||
clientY: height / 2,
|
clientY: height / 2,
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ describe('WaveformChart', () => {
|
|||||||
expect(domain[1]).toBeLessThanOrEqual(1)
|
expect(domain[1]).toBeLessThanOrEqual(1)
|
||||||
expect(endpoints()[0]).not.toBe(initialEndpoints[0])
|
expect(endpoints()[0]).not.toBe(initialEndpoints[0])
|
||||||
expect(endpoints()[1]).toBe(initialEndpoints[1])
|
expect(endpoints()[1]).toBe(initialEndpoints[1])
|
||||||
|
|
||||||
|
overlays[0].element.dispatchEvent(
|
||||||
|
new WheelEvent('wheel', {
|
||||||
|
deltaY: 4_000,
|
||||||
|
clientX: firstWidth - 1,
|
||||||
|
clientY: 130,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
flushAnimationFrames()
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
expect(endpoints()[0]).toBe(initialEndpoints[0])
|
||||||
|
expect(endpoints()[1]).toBe(initialEndpoints[1])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('emits one zoom-end payload after a shared zoom gesture completes', async () => {
|
it('emits one zoom-end payload after a shared zoom gesture completes', async () => {
|
||||||
|
|||||||
@@ -79,8 +79,8 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
expect(wrapper.emitted('zoom-change')).toBeUndefined()
|
expect(wrapper.emitted('zoom-change')).toBeUndefined()
|
||||||
expect(wrapper.emitted('zoom-end')).toBeUndefined()
|
expect(wrapper.emitted('zoom-end')).toBeUndefined()
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('ignores independent viewport dragging', async () => {
|
it('ignores independent viewport dragging', async () => {
|
||||||
@@ -207,7 +207,7 @@ describe('WaveformChart', () => {
|
|||||||
expect(secondDomain[1] - secondDomain[0]).toBeCloseTo(10)
|
expect(secondDomain[1] - secondDomain[0]).toBeCloseTo(10)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps one global domain while loading narrower and wider windows', async () => {
|
it('zooms a shared viewport back out after loading a narrower data window', async () => {
|
||||||
const wrapper = await mountSizedChart(
|
const wrapper = await mountSizedChart(
|
||||||
{
|
{
|
||||||
kind: 'points',
|
kind: 'points',
|
||||||
@@ -256,9 +256,57 @@ describe('WaveformChart', () => {
|
|||||||
|
|
||||||
const eventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
const eventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
||||||
await dispatchWheel(4000)
|
await dispatchWheel(4000)
|
||||||
const currentDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
const zoomedOutDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
||||||
|
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
|
||||||
|
expect(zoomedOutDomain[1] - zoomedOutDomain[0]).toBeCloseTo(100)
|
||||||
|
|
||||||
|
const fullDomainEventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
||||||
|
await dispatchWheel(4000)
|
||||||
|
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(fullDomainEventCount)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows zooming out when the minimum visible point count blocks zooming in', async () => {
|
||||||
|
const wrapper = await mountSizedChart({
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 1, y: 10 },
|
||||||
|
{ x: 2, y: 20 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const overlay = wrapper.get('.waveform-chart__overlay--independent')
|
||||||
|
const width = Number(overlay.attributes('width'))
|
||||||
|
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
||||||
|
value: () => ({ left: 0, top: 0, width, height: 290 }),
|
||||||
|
})
|
||||||
|
const dispatchWheel = async (deltaY: number) => {
|
||||||
|
overlay.element.dispatchEvent(
|
||||||
|
new WheelEvent('wheel', {
|
||||||
|
deltaY,
|
||||||
|
clientX: width / 2,
|
||||||
|
clientY: 145,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
flushAnimationFrames()
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
|
||||||
|
await dispatchWheel(-4000)
|
||||||
|
const zoomedDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
||||||
|
expect(zoomedDomain[1] - zoomedDomain[0]).toBeCloseTo(0.05)
|
||||||
|
|
||||||
|
await wrapper.setProps({ minVisiblePoints: 10 })
|
||||||
|
await flushPromises()
|
||||||
|
const eventCount = wrapper.emitted('zoom-change')?.length ?? 0
|
||||||
|
await dispatchWheel(-1000)
|
||||||
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount)
|
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount)
|
||||||
expect(currentDomain[1] - currentDomain[0]).toBeCloseTo(2.5)
|
|
||||||
|
await dispatchWheel(4000)
|
||||||
|
const zoomedOutDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
||||||
|
expect(wrapper.emitted('zoom-change')?.length ?? 0).toBe(eventCount + 1)
|
||||||
|
expect(zoomedOutDomain).toEqual([0, 2])
|
||||||
})
|
})
|
||||||
|
|
||||||
it('resets a shared viewport on double-click and emits zoom-reset', async () => {
|
it('resets a shared viewport on double-click and emits zoom-reset', async () => {
|
||||||
@@ -285,14 +333,14 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
flushAnimationFrames()
|
flushAnimationFrames()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).not.toBe('0')
|
||||||
|
|
||||||
overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
|
overlay.element.dispatchEvent(new MouseEvent('dblclick', { bubbles: true, cancelable: true }))
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
|
expect(wrapper.emitted('zoom-reset')).toHaveLength(1)
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--start').text()).toBe('0')
|
||||||
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2.00')
|
expect(wrapper.get('.waveform-chart__axis-endpoint--end').text()).toBe('2000')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('exposes resetViewport for independent tracks', async () => {
|
it('exposes resetViewport for independent tracks', async () => {
|
||||||
@@ -316,13 +364,13 @@ describe('WaveformChart', () => {
|
|||||||
)
|
)
|
||||||
flushAnimationFrames()
|
flushAnimationFrames()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0.00')
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).not.toBe('0')
|
||||||
|
|
||||||
const chart = wrapper.vm as unknown as { resetViewport: () => void }
|
const chart = wrapper.vm as unknown as { resetViewport: () => void }
|
||||||
chart.resetViewport()
|
chart.resetViewport()
|
||||||
await flushPromises()
|
await flushPromises()
|
||||||
|
|
||||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0.00')
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint--start')[0].text()).toBe('0')
|
||||||
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1.00')
|
expect(wrapper.findAll('.waveform-chart__axis-endpoint--end')[0].text()).toBe('1000')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
114
src/components/waveformChartCases/wheelZoomOut.test.ts
Normal file
114
src/components/waveformChartCases/wheelZoomOut.test.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { flushPromises } from '@vue/test-utils'
|
||||||
|
import { describe, expect, it, vi } from 'vitest'
|
||||||
|
|
||||||
|
import { flushAnimationFrames } from '../../test/setup'
|
||||||
|
import { mountSizedChart } from '../../test/waveformChart'
|
||||||
|
|
||||||
|
describe('WaveformChart wheel zoom out', () => {
|
||||||
|
it('restores an independent initial domain after replacing data with a narrow window', async () => {
|
||||||
|
const createData = (start: number, end: number) => ({
|
||||||
|
kind: 'series' as const,
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
id: 'channel-a',
|
||||||
|
name: '通道 A',
|
||||||
|
data: {
|
||||||
|
kind: 'points' as const,
|
||||||
|
points: [
|
||||||
|
{ x: start, y: 0 },
|
||||||
|
{ x: end, y: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const wrapper = await mountSizedChart(createData(-5, 5), {
|
||||||
|
displayMode: 'independent',
|
||||||
|
initialXDomain: [-5, 5],
|
||||||
|
})
|
||||||
|
const overlay = wrapper.get('.waveform-chart__overlay--independent')
|
||||||
|
const overlayWidth = Number(overlay.attributes('width'))
|
||||||
|
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
||||||
|
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
||||||
|
})
|
||||||
|
const dispatchWheel = async (deltaY: number) => {
|
||||||
|
overlay.element.dispatchEvent(
|
||||||
|
new WheelEvent('wheel', {
|
||||||
|
deltaY,
|
||||||
|
clientX: overlayWidth / 2,
|
||||||
|
clientY: 145,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
flushAnimationFrames()
|
||||||
|
await flushPromises()
|
||||||
|
}
|
||||||
|
const endpoints = () => ({
|
||||||
|
start: wrapper.get('.waveform-chart__axis-endpoint--start').text(),
|
||||||
|
end: wrapper.get('.waveform-chart__axis-endpoint--end').text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
||||||
|
await dispatchWheel(-4000)
|
||||||
|
expect(endpoints()).not.toEqual({ start: '-5000', end: '5000' })
|
||||||
|
|
||||||
|
await wrapper.setProps({ data: createData(-0.125, 0.125) })
|
||||||
|
await flushPromises()
|
||||||
|
await dispatchWheel(4000)
|
||||||
|
|
||||||
|
expect(endpoints()).toEqual({ start: '-5000', end: '5000' })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('emits one zoom-end payload after zooming a shared viewport out', async () => {
|
||||||
|
vi.useFakeTimers()
|
||||||
|
try {
|
||||||
|
const wrapper = await mountSizedChart({
|
||||||
|
kind: 'points',
|
||||||
|
points: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 2, y: 1 },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
const overlay = wrapper.get('.waveform-chart__overlay')
|
||||||
|
const overlayWidth = Number(overlay.attributes('width'))
|
||||||
|
Object.defineProperty(overlay.element, 'getBoundingClientRect', {
|
||||||
|
value: () => ({ left: 0, top: 0, width: overlayWidth, height: 290 }),
|
||||||
|
})
|
||||||
|
const dispatchWheel = (deltaY: number) =>
|
||||||
|
overlay.element.dispatchEvent(
|
||||||
|
new WheelEvent('wheel', {
|
||||||
|
deltaY,
|
||||||
|
clientX: overlayWidth / 2,
|
||||||
|
clientY: 145,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
dispatchWheel(-4000)
|
||||||
|
flushAnimationFrames()
|
||||||
|
await vi.advanceTimersByTimeAsync(200)
|
||||||
|
await flushPromises()
|
||||||
|
const zoomedDomain = wrapper.emitted('zoom-change')?.at(-1)?.[0] as [number, number]
|
||||||
|
const zoomedSpan = zoomedDomain[1] - zoomedDomain[0]
|
||||||
|
|
||||||
|
dispatchWheel(1000)
|
||||||
|
flushAnimationFrames()
|
||||||
|
expect(wrapper.emitted('zoom-end')).toHaveLength(1)
|
||||||
|
await vi.advanceTimersByTimeAsync(200)
|
||||||
|
await flushPromises()
|
||||||
|
|
||||||
|
const zoomedOutPayload = wrapper.emitted('zoom-end')?.at(-1)?.[0] as {
|
||||||
|
start: number
|
||||||
|
end: number
|
||||||
|
gesture: string
|
||||||
|
}
|
||||||
|
expect(wrapper.emitted('zoom-end')).toHaveLength(2)
|
||||||
|
expect(zoomedOutPayload.end - zoomedOutPayload.start).toBeGreaterThan(zoomedSpan)
|
||||||
|
expect(zoomedOutPayload.gesture).toBe('wheel')
|
||||||
|
} finally {
|
||||||
|
vi.useRealTimers()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -3,69 +3,78 @@ import { describe, expect, it } from 'vitest'
|
|||||||
import {
|
import {
|
||||||
formatAnnotationTime,
|
formatAnnotationTime,
|
||||||
formatAxisTime,
|
formatAxisTime,
|
||||||
formatAxisTimeExponent,
|
|
||||||
formatEndpointTime,
|
formatEndpointTime,
|
||||||
formatPlainNumber,
|
formatPlainNumber,
|
||||||
formatScientificAxisExponent,
|
formatScientificAxisExponent,
|
||||||
formatScientificAxisLabel,
|
formatScientificAxisLabel,
|
||||||
formatTooltipNumber,
|
formatTooltipNumber,
|
||||||
|
formatTooltipTime,
|
||||||
|
resolveScientificAxisExponent,
|
||||||
shouldUseScientificAxisLabel,
|
shouldUseScientificAxisLabel,
|
||||||
} from './formatters'
|
} from './formatters'
|
||||||
|
|
||||||
describe('waveform number formatters', () => {
|
describe('waveform number formatters', () => {
|
||||||
it('uses the reference Y-axis scientific notation boundaries', () => {
|
it('uses the reference Y-axis scientific notation boundaries', () => {
|
||||||
expect(shouldUseScientificAxisLabel(0)).toBe(false)
|
expect(shouldUseScientificAxisLabel(0)).toBe(false)
|
||||||
expect(shouldUseScientificAxisLabel(0.009)).toBe(true)
|
expect(shouldUseScientificAxisLabel(0.000999)).toBe(true)
|
||||||
expect(shouldUseScientificAxisLabel(0.01)).toBe(false)
|
expect(shouldUseScientificAxisLabel(0.001)).toBe(false)
|
||||||
expect(shouldUseScientificAxisLabel(99.99)).toBe(false)
|
expect(shouldUseScientificAxisLabel(999.999)).toBe(false)
|
||||||
expect(shouldUseScientificAxisLabel(100)).toBe(true)
|
expect(shouldUseScientificAxisLabel(1000)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('shares one separate exponent across an axis', () => {
|
it('prefixes one shared exponent to the largest visible tick', () => {
|
||||||
const positiveAxis = { axisMin: 0, axisMax: 254 }
|
const positiveAxis = { axisMin: 1000, axisMax: 3000 }
|
||||||
expect(formatScientificAxisLabel(127, positiveAxis)).toBe('1.27')
|
expect(formatScientificAxisLabel(1000, positiveAxis)).toBe('1')
|
||||||
expect(formatScientificAxisLabel(254, positiveAxis)).toBe('2.54')
|
expect(formatScientificAxisLabel(2000, positiveAxis)).toBe('2')
|
||||||
expect(formatScientificAxisExponent(0, 254)).toBe('E+02')
|
expect(formatScientificAxisLabel(3000, positiveAxis)).toBe('E+03 3')
|
||||||
|
expect(formatScientificAxisLabel(2000, { ...positiveAxis, topTickValue: 2000 })).toBe('E+03 2')
|
||||||
|
expect(formatScientificAxisExponent(1000, 3000)).toBe('E+03')
|
||||||
expect(formatScientificAxisExponent(0, 1e120)).toBe('E+120')
|
expect(formatScientificAxisExponent(0, 1e120)).toBe('E+120')
|
||||||
|
|
||||||
const tinyAxis = { axisMin: 0, axisMax: 0.0002 }
|
const tinyAxis = { axisMin: 0.0001, axisMax: 0.0003 }
|
||||||
expect(formatScientificAxisLabel(0.0001, tinyAxis)).toBe('1.00')
|
expect(formatScientificAxisLabel(0.0001, tinyAxis)).toBe('1')
|
||||||
expect(formatScientificAxisLabel(0.0002, tinyAxis)).toBe('2.00')
|
expect(formatScientificAxisLabel(0.0002, tinyAxis)).toBe('2')
|
||||||
expect(formatScientificAxisExponent(0, 0.0002)).toBe('E-04')
|
expect(formatScientificAxisLabel(0.0003, tinyAxis)).toBe('E-04 3')
|
||||||
|
expect(formatScientificAxisExponent(0.0001, 0.0003)).toBe('E-04')
|
||||||
const negativeAxis = { axisMin: -254, axisMax: 0 }
|
|
||||||
expect(formatScientificAxisLabel(-254, negativeAxis)).toBe('-2.54')
|
|
||||||
expect(formatScientificAxisLabel(0, negativeAxis)).toBe('0.00')
|
|
||||||
expect(formatScientificAxisExponent(-254, 0)).toBe('E+02')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it('keeps plain axes at two decimals and removes negative zero', () => {
|
it('derives the exponent from Math.max(axisMin, axisMax)', () => {
|
||||||
expect(formatScientificAxisLabel(99.99, { axisMin: 0, axisMax: 99.99 })).toBe('99.99')
|
expect(resolveScientificAxisExponent(-9000, -1000)).toBe(3)
|
||||||
expect(formatScientificAxisLabel(0.01, { axisMin: 0, axisMax: 0.01 })).toBe('0.01')
|
expect(resolveScientificAxisExponent(-10_000, 3000)).toBe(3)
|
||||||
expect(formatScientificAxisLabel(-0.001, { axisMin: -1, axisMax: 1 })).toBe('0.00')
|
expect(resolveScientificAxisExponent(-1000, 0)).toBeNull()
|
||||||
|
expect(resolveScientificAxisExponent(0, 0)).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('keeps five significant digits, removes trailing zeroes, and limits decimals to four', () => {
|
||||||
|
expect(formatScientificAxisLabel(123.456, { axisMin: 0, axisMax: 999 })).toBe('123.46')
|
||||||
|
expect(formatScientificAxisLabel(1234.56, { axisMin: 0, axisMax: 9999 })).toBe('1.2346')
|
||||||
|
expect(formatScientificAxisLabel(3000, { axisMin: 1000, axisMax: 3000 })).toBe('E+03 3')
|
||||||
|
expect(formatScientificAxisLabel(-0, { axisMin: -1, axisMax: 1 })).toBe('0')
|
||||||
expect(formatScientificAxisLabel(Number.NaN)).toBe('NaN')
|
expect(formatScientificAxisLabel(Number.NaN)).toBe('NaN')
|
||||||
expect(formatScientificAxisLabel(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
expect(formatScientificAxisLabel(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
||||||
expect(formatScientificAxisExponent(0, 0)).toBeNull()
|
expect(formatScientificAxisExponent(0, 0)).toBeNull()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('formats X-axis ticks and endpoints from the selected display unit', () => {
|
it('formats X-axis ticks and endpoints as plain integers in the selected display unit', () => {
|
||||||
const domain: [number, number] = [0, 1]
|
const domain: [number, number] = [0, 1]
|
||||||
expect(formatAxisTime(0.5, 'ms', domain)).toBe('0.50')
|
expect(formatAxisTime(0.5004, 'ms', domain)).toBe('500')
|
||||||
expect(formatEndpointTime(1, domain, 'ms')).toBe('1.00')
|
expect(formatEndpointTime(1, domain, 'ms')).toBe('1000')
|
||||||
expect(formatAxisTimeExponent(domain, 'ms')).toBe('E+03')
|
expect(formatAxisTime(0.5, 's', domain)).toBe('1')
|
||||||
expect(formatAxisTime(0.5, 's', domain)).toBe('0.50')
|
expect(formatEndpointTime(1, domain, 's')).toBe('1')
|
||||||
expect(formatEndpointTime(1, domain, 's')).toBe('1.00')
|
|
||||||
expect(formatAxisTimeExponent(domain, 's')).toBeNull()
|
|
||||||
|
|
||||||
const tinyDomain: [number, number] = [0, 0.000001]
|
const tinyDomain: [number, number] = [0, 0.000001]
|
||||||
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('1.00')
|
expect(formatEndpointTime(0.000001, tinyDomain, 's')).toBe('0')
|
||||||
expect(formatAxisTimeExponent(tinyDomain, 's')).toBe('E-06')
|
expect(formatAxisTime(-0.000001, 's', tinyDomain)).toBe('0')
|
||||||
|
expect(formatAxisTime(1e21, 's')).toBe('1000000000000000000000')
|
||||||
|
expect(formatAxisTime(Number.POSITIVE_INFINITY, 's')).toBe('Infinity')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('formats tooltip and raw values for their display contexts', () => {
|
it('formats tooltip and raw values for their display contexts', () => {
|
||||||
expect(formatTooltipNumber(12345.67891)).toBe('12,345.6789')
|
expect(formatTooltipNumber(12345.67891)).toBe('12,345.6789')
|
||||||
expect(formatTooltipNumber(-0)).toBe('0')
|
expect(formatTooltipNumber(-0)).toBe('0')
|
||||||
expect(formatTooltipNumber(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
expect(formatTooltipNumber(Number.POSITIVE_INFINITY)).toBe('Infinity')
|
||||||
|
expect(formatTooltipTime(1.234567, 's')).toBe('1.2346')
|
||||||
|
expect(formatTooltipTime(1, 'ms')).toBe('1,000')
|
||||||
expect(formatPlainNumber(0.0000001)).toBe('0.0000001')
|
expect(formatPlainNumber(0.0000001)).toBe('0.0000001')
|
||||||
expect(formatPlainNumber(1e21)).toBe('1000000000000000000000')
|
expect(formatPlainNumber(1e21)).toBe('1000000000000000000000')
|
||||||
expect(formatPlainNumber(-0)).toBe('0')
|
expect(formatPlainNumber(-0)).toBe('0')
|
||||||
|
|||||||
@@ -4,22 +4,28 @@
|
|||||||
export type TimeUnit = 'ms' | 's'
|
export type TimeUnit = 'ms' | 's'
|
||||||
|
|
||||||
export interface ScientificAxisLabelOptions {
|
export interface ScientificAxisLabelOptions {
|
||||||
|
/** @deprecated Y-axis labels always use five significant digits before localization. */
|
||||||
precision?: number
|
precision?: number
|
||||||
axisMin?: number
|
axisMin?: number
|
||||||
axisMax?: number
|
axisMax?: number
|
||||||
}
|
|
||||||
|
|
||||||
/** @deprecated Use ScientificAxisLabelOptions. */
|
|
||||||
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions & {
|
|
||||||
topTickValue?: number
|
topTickValue?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_Y_AXIS_PRECISION = 2
|
/** @deprecated Use ScientificAxisLabelOptions. */
|
||||||
const SCIENTIFIC_MIN_ABSOLUTE_VALUE = 0.01
|
export type ScientificYAxisLabelOptions = ScientificAxisLabelOptions
|
||||||
const SCIENTIFIC_MAX_PLAIN_ABSOLUTE_VALUE = 100
|
|
||||||
|
const SCIENTIFIC_MIN_ABSOLUTE_VALUE = 0.001
|
||||||
|
const SCIENTIFIC_MAX_PLAIN_ABSOLUTE_VALUE = 1000
|
||||||
|
const Y_AXIS_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
||||||
|
maximumFractionDigits: 4,
|
||||||
|
})
|
||||||
const TOOLTIP_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
const TOOLTIP_NUMBER_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
||||||
maximumFractionDigits: 4,
|
maximumFractionDigits: 4,
|
||||||
})
|
})
|
||||||
|
const X_AXIS_TIME_FORMATTER = new Intl.NumberFormat('zh-CN', {
|
||||||
|
maximumFractionDigits: 0,
|
||||||
|
useGrouping: false,
|
||||||
|
})
|
||||||
|
|
||||||
function formatFixedNumber(value: number, precision: number): string {
|
function formatFixedNumber(value: number, precision: number): string {
|
||||||
const formatted = value.toFixed(Math.max(0, precision))
|
const formatted = value.toFixed(Math.max(0, precision))
|
||||||
@@ -38,9 +44,10 @@ export function shouldUseScientificAxisLabel(maxAbsoluteValue: number): boolean
|
|||||||
export function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null {
|
export function resolveScientificAxisExponent(axisMin?: number, axisMax?: number): number | null {
|
||||||
if (typeof axisMin !== 'number' || typeof axisMax !== 'number') return null
|
if (typeof axisMin !== 'number' || typeof axisMax !== 'number') return null
|
||||||
|
|
||||||
const maxAbsoluteValue = Math.max(Math.abs(axisMin), Math.abs(axisMax))
|
const maxValue = Math.max(axisMin, axisMax)
|
||||||
return shouldUseScientificAxisLabel(maxAbsoluteValue)
|
const absoluteMaxValue = Math.abs(maxValue)
|
||||||
? Math.floor(Math.log10(maxAbsoluteValue))
|
return shouldUseScientificAxisLabel(absoluteMaxValue)
|
||||||
|
? Math.floor(Math.log10(absoluteMaxValue))
|
||||||
: null
|
: null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +56,13 @@ function formatExponent(exponent: number): string {
|
|||||||
return `E${sign}${Math.abs(exponent).toString().padStart(2, '0')}`
|
return `E${sign}${Math.abs(exponent).toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatYAxisNumber(value: number): string {
|
||||||
|
if (Object.is(value, -0)) return '0'
|
||||||
|
const roundedValue = value === 0 ? 0 : Number(value.toPrecision(5))
|
||||||
|
const formatted = Y_AXIS_NUMBER_FORMATTER.format(roundedValue)
|
||||||
|
return formatted === '-0' ? '0' : formatted
|
||||||
|
}
|
||||||
|
|
||||||
/** Format a Y-axis tick, sharing one exponent derived from the complete axis domain. */
|
/** Format a Y-axis tick, sharing one exponent derived from the complete axis domain. */
|
||||||
export function formatScientificAxisLabel(
|
export function formatScientificAxisLabel(
|
||||||
value: number,
|
value: number,
|
||||||
@@ -56,10 +70,13 @@ export function formatScientificAxisLabel(
|
|||||||
): string {
|
): string {
|
||||||
if (!Number.isFinite(value)) return String(value)
|
if (!Number.isFinite(value)) return String(value)
|
||||||
|
|
||||||
const precision = options.precision ?? DEFAULT_Y_AXIS_PRECISION
|
|
||||||
const exponent = resolveScientificAxisExponent(options.axisMin, options.axisMax)
|
const exponent = resolveScientificAxisExponent(options.axisMin, options.axisMax)
|
||||||
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
const scaledValue = exponent === null ? value : value / 10 ** exponent
|
||||||
return formatFixedNumber(scaledValue, precision)
|
const formattedValue = formatYAxisNumber(scaledValue)
|
||||||
|
const topTickValue = options.topTickValue ?? options.axisMax
|
||||||
|
return exponent !== null && value === topTickValue
|
||||||
|
? `${formatExponent(exponent)} ${formattedValue}`
|
||||||
|
: 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. */
|
||||||
@@ -86,6 +103,13 @@ export function formatTooltipNumber(value: number): string {
|
|||||||
return TOOLTIP_NUMBER_FORMATTER.format(value)
|
return TOOLTIP_NUMBER_FORMATTER.format(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Format an X-axis time value as a plain integer without grouping separators. */
|
||||||
|
function formatAxisTimeValue(value: number): string {
|
||||||
|
if (!Number.isFinite(value)) return String(value)
|
||||||
|
const formatted = X_AXIS_TIME_FORMATTER.format(value)
|
||||||
|
return formatted === '-0' ? '0' : formatted
|
||||||
|
}
|
||||||
|
|
||||||
/** Convert a number to complete plain decimal text without forcing exponential notation. */
|
/** Convert a number to complete plain decimal text without forcing exponential notation. */
|
||||||
export function formatPlainNumber(value: number): string {
|
export function formatPlainNumber(value: number): string {
|
||||||
if (!Number.isFinite(value)) return String(value)
|
if (!Number.isFinite(value)) return String(value)
|
||||||
@@ -134,54 +158,34 @@ export function endpointFractionDigits(domain: [number, number], timeUnit: TimeU
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按完整 X 轴显示域格式化端点时间
|
* 将 X 轴端点时间格式化为当前显示单位下的普通整数
|
||||||
* @param value 时间值(秒)
|
* @param value 时间值(秒)
|
||||||
* @param domain 数据域
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
||||||
* @param timeUnit 时间单位
|
* @param timeUnit 时间单位
|
||||||
* @returns 格式化的时间字符串
|
* @returns 格式化的时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatEndpointTime(
|
export function formatEndpointTime(
|
||||||
value: number,
|
value: number,
|
||||||
domain: [number, number],
|
_domain: [number, number],
|
||||||
timeUnit: TimeUnit,
|
timeUnit: TimeUnit,
|
||||||
): string {
|
): string {
|
||||||
const [axisMin, axisMax] = domain.map((domainValue) => displayTime(domainValue, timeUnit)) as [
|
return formatAxisTimeValue(displayTime(value, timeUnit))
|
||||||
number,
|
|
||||||
number,
|
|
||||||
]
|
|
||||||
return formatScientificAxisLabel(displayTime(value, timeUnit), {
|
|
||||||
axisMin,
|
|
||||||
axisMax,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按完整 X 轴显示域格式化时间刻度
|
* 将 X 轴时间刻度格式化为当前显示单位下的普通整数
|
||||||
* @param value 时间值(秒)
|
* @param value 时间值(秒)
|
||||||
* @param timeUnit 时间单位
|
* @param timeUnit 时间单位
|
||||||
|
* @param _domain 数据域(为保持现有调用签名而保留)
|
||||||
* @returns 格式化的时间字符串
|
* @returns 格式化的时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatAxisTime(
|
export function formatAxisTime(
|
||||||
value: number,
|
value: number,
|
||||||
timeUnit: TimeUnit,
|
timeUnit: TimeUnit,
|
||||||
domain?: [number, number],
|
_domain?: [number, number],
|
||||||
): string {
|
): string {
|
||||||
const displayValue = displayTime(value, timeUnit)
|
void _domain
|
||||||
const displayDomain = domain?.map((domainValue) => displayTime(domainValue, timeUnit)) as
|
return formatAxisTimeValue(displayTime(value, timeUnit))
|
||||||
[number, number] | undefined
|
|
||||||
return formatScientificAxisLabel(displayValue, {
|
|
||||||
axisMin: displayDomain?.[0],
|
|
||||||
axisMax: displayDomain?.[1],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Return the shared multiplier for an X-axis domain in its selected display unit. */
|
|
||||||
export function formatAxisTimeExponent(
|
|
||||||
domain: [number, number],
|
|
||||||
timeUnit: TimeUnit,
|
|
||||||
): string | null {
|
|
||||||
const [axisMin, axisMax] = domain.map((value) => displayTime(value, timeUnit)) as [number, number]
|
|
||||||
return formatScientificAxisExponent(axisMin, axisMax)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -191,11 +195,7 @@ export function formatAxisTimeExponent(
|
|||||||
* @returns 格式化的时间字符串
|
* @returns 格式化的时间字符串
|
||||||
*/
|
*/
|
||||||
export function formatTooltipTime(value: number, timeUnit: TimeUnit): string {
|
export function formatTooltipTime(value: number, timeUnit: TimeUnit): string {
|
||||||
const displayValue = displayTime(value, timeUnit)
|
return formatTooltipNumber(displayTime(value, timeUnit))
|
||||||
return displayValue.toLocaleString('zh-CN', {
|
|
||||||
minimumFractionDigits: 4,
|
|
||||||
maximumFractionDigits: 4,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Format an annotation X coordinate in the selected display time unit. */
|
/** Format an annotation X coordinate in the selected display time unit. */
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export {
|
|||||||
endpointFractionDigits,
|
endpointFractionDigits,
|
||||||
formatEndpointTime,
|
formatEndpointTime,
|
||||||
formatAxisTime,
|
formatAxisTime,
|
||||||
formatAxisTimeExponent,
|
|
||||||
formatTooltipTime,
|
formatTooltipTime,
|
||||||
formatAnnotationTime,
|
formatAnnotationTime,
|
||||||
formatPlainNumber,
|
formatPlainNumber,
|
||||||
|
|||||||
Reference in New Issue
Block a user