feat(坐标轴): 支持显示纵轴单位
All checks were successful
Package component / package (push) Successful in 4m18s
All checks were successful
Package component / package (push) Successful in 4m18s
This commit is contained in:
30
README.md
30
README.md
@@ -298,6 +298,34 @@ chartRef.value?.setViewportDomain(trackDomain, trackIndex)
|
|||||||
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 的单位。
|
||||||
|
|
||||||
### 线型、点型与误差棒
|
### 线型、点型与误差棒
|
||||||
|
|
||||||
每条序列可以独立设置连线方式、数据点符号和误差棒:
|
每条序列可以独立设置连线方式、数据点符号和误差棒:
|
||||||
@@ -669,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.39",
|
"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",
|
||||||
|
|||||||
@@ -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 +
|
||||||
|
|||||||
@@ -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 = -(
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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