perf: 优化波形核心算法与图框网格控制
This commit is contained in:
@@ -124,6 +124,7 @@ describe('normalizeWaveformData', () => {
|
||||
|
||||
expect(series?.yDomain[0]).toBeLessThanOrEqual(-1)
|
||||
expect(series?.yDomain[1]).toBeGreaterThanOrEqual(6)
|
||||
expect(series?.hasErrorPoints).toBe(true)
|
||||
})
|
||||
|
||||
it('normalizes multiple named series and removes empty series', () => {
|
||||
@@ -2136,6 +2137,49 @@ describe('WaveformChart', () => {
|
||||
expect(wrapper.get('.waveform-chart__line').attributes('stroke')).toBe('#0960bd')
|
||||
})
|
||||
|
||||
it('controls horizontal and vertical grid lines independently by track ID', async () => {
|
||||
const data = gridSeries(2)
|
||||
if (data.kind === 'series') {
|
||||
data.series[0].trackId = 'frame-a'
|
||||
data.series[1].trackId = 'frame-b'
|
||||
}
|
||||
const wrapper = await mountSizedChart(data, {
|
||||
grid: {
|
||||
rowCount: 1,
|
||||
columnCount: 2,
|
||||
trackLines: {
|
||||
'frame-a': { horizontal: false },
|
||||
'frame-b': { vertical: false },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const tracks = wrapper.findAll('.waveform-chart__track')
|
||||
expect(tracks).toHaveLength(2)
|
||||
expect(tracks[0].findAll('[data-grid-direction="horizontal"]')).toHaveLength(0)
|
||||
expect(tracks[0].findAll('[data-grid-direction="vertical"]').length).not.toBe(0)
|
||||
expect(tracks[1].findAll('[data-grid-direction="vertical"]')).toHaveLength(0)
|
||||
expect(tracks[1].findAll('[data-grid-direction="horizontal"]').length).not.toBe(0)
|
||||
})
|
||||
|
||||
it('keeps per-track grid line visibility attached across pagination', async () => {
|
||||
const wrapper = await mountSizedChart(gridSeries(3), {
|
||||
grid: {
|
||||
rowCount: 1,
|
||||
columnCount: 1,
|
||||
trackLines: {
|
||||
'channel-1': { horizontal: false, vertical: false },
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('[data-grid-direction]')).not.toHaveLength(0)
|
||||
await wrapper.get('.ant-pagination-next button').trigger('click')
|
||||
expect(wrapper.findAll('[data-grid-direction]')).toHaveLength(0)
|
||||
await wrapper.get('.ant-pagination-next button').trigger('click')
|
||||
expect(wrapper.findAll('[data-grid-direction]')).not.toHaveLength(0)
|
||||
})
|
||||
|
||||
it('applies one custom frame style to every non-empty track', async () => {
|
||||
const wrapper = await mountSizedChart(gridSeries(2), {
|
||||
grid: { rowCount: 2, columnCount: 1 },
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type ZoomTransform,
|
||||
} from 'd3'
|
||||
import { resolveWaveformRenderingOptions } from '../core'
|
||||
import { hasMinimumVisibleXValues } from '../core/rendering'
|
||||
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../utils'
|
||||
import { useAnimationFrameThrottle } from './utils/useAnimationFrameThrottle'
|
||||
import {
|
||||
@@ -825,20 +826,13 @@ function resolveMaximumZoomScale(domain: [number, number]): number {
|
||||
return Math.min(40, Math.max(1, domainSpan / (minZoomSpan ?? domainSpan)))
|
||||
}
|
||||
|
||||
function visiblePointCount(track: TrackLayout): number {
|
||||
const [start, end] = track.xScale.domain()
|
||||
const xValues = new Set<number>()
|
||||
track.seriesList.forEach((series) => {
|
||||
series.points.forEach((point) => {
|
||||
if (point.x >= start && point.x <= end) xValues.add(point.x)
|
||||
})
|
||||
})
|
||||
return xValues.size
|
||||
}
|
||||
|
||||
function canZoomTrack(track: TrackLayout): boolean {
|
||||
const minimum = Number(props.minVisiblePoints)
|
||||
return !Number.isFinite(minimum) || minimum <= 0 || visiblePointCount(track) >= minimum
|
||||
return hasMinimumVisibleXValues(
|
||||
track.seriesList,
|
||||
track.xScale.domain() as [number, number],
|
||||
minimum,
|
||||
)
|
||||
}
|
||||
|
||||
function canZoomSharedTracks(): boolean {
|
||||
|
||||
@@ -11,11 +11,44 @@ import {
|
||||
|
||||
describe('waveform grid helpers', () => {
|
||||
it('normalizes grid counts and uses a two by one default', () => {
|
||||
expect(normalizeGridOptions()).toEqual({ rowCount: 2, columnCount: 1, showPagination: true })
|
||||
expect(normalizeGridOptions()).toEqual({
|
||||
rowCount: 2,
|
||||
columnCount: 1,
|
||||
showPagination: true,
|
||||
trackLines: {},
|
||||
})
|
||||
expect(normalizeGridOptions({ rowCount: 0, columnCount: 99 })).toEqual({
|
||||
rowCount: 1,
|
||||
columnCount: 10,
|
||||
showPagination: true,
|
||||
trackLines: {},
|
||||
})
|
||||
})
|
||||
|
||||
it('normalizes per-track grid line visibility with visible defaults', () => {
|
||||
expect(
|
||||
normalizeGridOptions({
|
||||
trackLines: {
|
||||
voltage: { horizontal: false },
|
||||
current: { vertical: false },
|
||||
},
|
||||
}).trackLines,
|
||||
).toEqual({
|
||||
voltage: { horizontal: false, vertical: true },
|
||||
current: { horizontal: true, vertical: false },
|
||||
})
|
||||
})
|
||||
|
||||
it('falls back to visible grid lines for invalid runtime values', () => {
|
||||
const options = {
|
||||
trackLines: {
|
||||
voltage: { horizontal: 'invalid', vertical: null },
|
||||
},
|
||||
} as unknown as Parameters<typeof normalizeGridOptions>[0]
|
||||
|
||||
expect(normalizeGridOptions(options).trackLines.voltage).toEqual({
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -8,12 +8,26 @@ export interface WaveformGridOptions {
|
||||
rowCount?: number
|
||||
columnCount?: number
|
||||
showPagination?: boolean
|
||||
trackLines?: WaveformGridTrackLines
|
||||
}
|
||||
|
||||
export interface WaveformGridLineOptions {
|
||||
horizontal?: boolean
|
||||
vertical?: boolean
|
||||
}
|
||||
|
||||
export type WaveformGridTrackLines = Record<string, WaveformGridLineOptions>
|
||||
|
||||
export interface NormalizedWaveformGridLineOptions {
|
||||
horizontal: boolean
|
||||
vertical: boolean
|
||||
}
|
||||
|
||||
export interface NormalizedWaveformGridOptions {
|
||||
rowCount: number
|
||||
columnCount: number
|
||||
showPagination: boolean
|
||||
trackLines: Record<string, NormalizedWaveformGridLineOptions>
|
||||
}
|
||||
|
||||
export interface GridCellGeometry {
|
||||
@@ -37,10 +51,20 @@ const normalizeCount = (value: unknown, fallback: number) => {
|
||||
}
|
||||
|
||||
export function normalizeGridOptions(options?: WaveformGridOptions): NormalizedWaveformGridOptions {
|
||||
const trackLines = Object.fromEntries(
|
||||
Object.entries(options?.trackLines ?? {}).map(([trackId, lines]) => [
|
||||
trackId,
|
||||
{
|
||||
horizontal: typeof lines?.horizontal === 'boolean' ? lines.horizontal : true,
|
||||
vertical: typeof lines?.vertical === 'boolean' ? lines.vertical : true,
|
||||
},
|
||||
]),
|
||||
)
|
||||
return {
|
||||
rowCount: normalizeCount(options?.rowCount, 2),
|
||||
columnCount: normalizeCount(options?.columnCount, 1),
|
||||
showPagination: options?.showPagination ?? true,
|
||||
trackLines,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ function series(id: string, minimum: number, maximum: number): DisplaySeries {
|
||||
],
|
||||
xDomain: [0, 1],
|
||||
yDomain: [minimum, maximum],
|
||||
hasErrorPoints: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +62,7 @@ function layoutForSeries(
|
||||
series: sourceTrack,
|
||||
},
|
||||
],
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false },
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
||||
displayMode: 'independent',
|
||||
overlayMode: 'single-axis',
|
||||
independentTransforms: [transform],
|
||||
@@ -95,7 +96,7 @@ describe('multi-value Y-axis grouping', () => {
|
||||
series: sourceTrack,
|
||||
},
|
||||
],
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false },
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
||||
displayMode: 'independent',
|
||||
overlayMode: 'single-axis',
|
||||
independentTransforms: [zoomIdentity],
|
||||
@@ -198,7 +199,7 @@ describe('multi-value Y-axis grouping', () => {
|
||||
series: track([series('left', 0, 254), series('right', 0, 254)]),
|
||||
},
|
||||
],
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false },
|
||||
grid: { rowCount: 1, columnCount: 1, showPagination: false, trackLines: {} },
|
||||
displayMode: 'independent',
|
||||
overlayMode: 'multi-axis',
|
||||
independentTransforms: [zoomIdentity],
|
||||
@@ -241,6 +242,7 @@ describe('decoration sampling', () => {
|
||||
error: index % 200 === 1 ? 0.1 : 0,
|
||||
})),
|
||||
xDomain: [0, 999],
|
||||
hasErrorPoints: true,
|
||||
})
|
||||
|
||||
it('shares prioritized source points between dense symbols and error bars', () => {
|
||||
@@ -262,6 +264,7 @@ describe('decoration sampling', () => {
|
||||
it('keeps standalone, zero-error, and non-downsampled decoration behavior', () => {
|
||||
const noErrors = denseSeries()
|
||||
noErrors.points = noErrors.points.map((point) => ({ x: point.x, y: point.y }))
|
||||
noErrors.hasErrorPoints = false
|
||||
const zeroErrorPath = layoutForSeries(noErrors)
|
||||
expect(zeroErrorPath.errorBarRenderPoints).toEqual([])
|
||||
expect(zeroErrorPath.pointRenderPoints.length).toBeLessThanOrEqual(Math.ceil(120 / 10) + 2)
|
||||
|
||||
@@ -9,11 +9,9 @@ import {
|
||||
} from 'd3'
|
||||
|
||||
import {
|
||||
selectDecorationPoints,
|
||||
selectRenderablePoints,
|
||||
resolveWaveformPointErrors,
|
||||
selectSeriesRenderPoints,
|
||||
type ResolvedWaveformRenderingOptions,
|
||||
} from '../../core'
|
||||
} from '../../core/rendering'
|
||||
import type { WaveformDisplayMode, WaveformOverlayMode, WaveformPoint } from '../../types'
|
||||
import {
|
||||
buildMinorTicks,
|
||||
@@ -217,6 +215,7 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
||||
points: [],
|
||||
xDomain: [0, 1],
|
||||
yDomain: [0, 1],
|
||||
hasErrorPoints: false,
|
||||
}
|
||||
const displayTrack: DisplayTrack = cell.series ?? {
|
||||
id: emptySeries.id,
|
||||
@@ -317,51 +316,18 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
||||
axis.seriesList.some((series) => series.id === trackSeries.id),
|
||||
)
|
||||
const seriesYScale = yAxis?.scale ?? yScale
|
||||
const pathPoints = selectRenderablePoints(
|
||||
const renderPoints = selectSeriesRenderPoints(
|
||||
trackSeries.points,
|
||||
domain,
|
||||
cell.width,
|
||||
options.rendering,
|
||||
{
|
||||
lineVisible: !isEmpty && trackSeries.lineType !== 'none',
|
||||
pointVisible: trackSeries.pointType !== 'none',
|
||||
errorBarVisible: trackSeries.errorBar.visible,
|
||||
hasErrorPoints: trackSeries.hasErrorPoints,
|
||||
},
|
||||
)
|
||||
const hasError = (point: WaveformPoint) => {
|
||||
const { lower, upper } = resolveWaveformPointErrors(point)
|
||||
return lower !== 0 || upper !== 0
|
||||
}
|
||||
const hasErrorPoints = trackSeries.errorBar.visible && trackSeries.points.some(hasError)
|
||||
const sharesDecorationPoints = trackSeries.pointType !== 'none' && hasErrorPoints
|
||||
const sharedDecorationPoints = sharesDecorationPoints
|
||||
? selectDecorationPoints(
|
||||
trackSeries.points,
|
||||
domain,
|
||||
cell.width,
|
||||
Math.max(options.rendering.pointMinSpacing, options.rendering.errorBarMinSpacing),
|
||||
options.rendering.downsample,
|
||||
undefined,
|
||||
hasError,
|
||||
)
|
||||
: undefined
|
||||
const pointRenderPoints =
|
||||
trackSeries.pointType === 'none'
|
||||
? []
|
||||
: (sharedDecorationPoints ??
|
||||
selectDecorationPoints(
|
||||
trackSeries.points,
|
||||
domain,
|
||||
cell.width,
|
||||
options.rendering.pointMinSpacing,
|
||||
options.rendering.downsample,
|
||||
))
|
||||
const errorBarRenderPoints = trackSeries.errorBar.visible
|
||||
? (sharedDecorationPoints?.filter(hasError) ??
|
||||
selectDecorationPoints(
|
||||
trackSeries.points,
|
||||
domain,
|
||||
cell.width,
|
||||
options.rendering.errorBarMinSpacing,
|
||||
options.rendering.downsample,
|
||||
hasError,
|
||||
))
|
||||
: []
|
||||
const pathGenerator = line<WaveformPoint>()
|
||||
.x((point) => xScale(point.x))
|
||||
.y((point) => seriesYScale(point.y))
|
||||
@@ -372,9 +338,9 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
||||
}
|
||||
return {
|
||||
series: trackSeries,
|
||||
path: isEmpty || trackSeries.lineType === 'none' ? null : pathGenerator(pathPoints),
|
||||
pointRenderPoints,
|
||||
errorBarRenderPoints,
|
||||
path: renderPoints.linePoints.length ? pathGenerator(renderPoints.linePoints) : null,
|
||||
pointRenderPoints: renderPoints.pointRenderPoints,
|
||||
errorBarRenderPoints: renderPoints.errorBarRenderPoints,
|
||||
yScale: seriesYScale,
|
||||
yAxisIndex: yAxis?.index ?? 0,
|
||||
}
|
||||
@@ -407,6 +373,10 @@ export function buildTrackLayouts(options: BuildTrackLayoutsOptions): TrackLayou
|
||||
xAxisExponent,
|
||||
path: seriesPaths[0]?.path ?? null,
|
||||
seriesPaths,
|
||||
gridLines: options.grid.trackLines[displayTrack.id] ?? {
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
},
|
||||
showXAxis:
|
||||
(isEmpty || hasVisibleSeries) &&
|
||||
(options.displayMode === 'independent' ||
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
WaveformPoint,
|
||||
WaveformPointType,
|
||||
} from '../../types'
|
||||
import type { NormalizedWaveformGridLineOptions } from './grid'
|
||||
|
||||
/**
|
||||
* 显示系列
|
||||
@@ -21,6 +22,7 @@ export interface DisplaySeries {
|
||||
points: WaveformPoint[]
|
||||
xDomain: [number, number]
|
||||
yDomain: [number, number]
|
||||
hasErrorPoints: boolean
|
||||
}
|
||||
|
||||
export interface DisplayTrack {
|
||||
@@ -97,6 +99,7 @@ export interface TrackLayout {
|
||||
path: string | null
|
||||
seriesPaths: TrackSeriesPath[]
|
||||
showXAxis: boolean
|
||||
gridLines: NormalizedWaveformGridLineOptions
|
||||
}
|
||||
|
||||
// 重新导出 WaveformPoint 方便使用
|
||||
|
||||
@@ -22,34 +22,42 @@ export interface PreparedWaveformSeries {
|
||||
points: WaveformPoint[]
|
||||
xDomain: [number, number]
|
||||
yDomain: [number, number]
|
||||
hasErrorPoints: boolean
|
||||
}
|
||||
|
||||
function pointDomain(
|
||||
function pointMetrics(
|
||||
points: WaveformPoint[],
|
||||
key: 'x' | 'y',
|
||||
includeErrors = false,
|
||||
): [number, number] {
|
||||
let minimum = Number.POSITIVE_INFINITY
|
||||
let maximum = Number.NEGATIVE_INFINITY
|
||||
points.forEach((point) => {
|
||||
const value = point[key]
|
||||
if (value < minimum) minimum = value
|
||||
if (value > maximum) maximum = value
|
||||
if (key === 'y' && includeErrors) {
|
||||
): { xDomain: [number, number]; yDomain: [number, number]; hasErrorPoints: boolean } {
|
||||
let xMinimum = Number.POSITIVE_INFINITY
|
||||
let xMaximum = Number.NEGATIVE_INFINITY
|
||||
let yMinimum = Number.POSITIVE_INFINITY
|
||||
let yMaximum = Number.NEGATIVE_INFINITY
|
||||
let hasErrorPoints = false
|
||||
for (const point of points) {
|
||||
if (point.x < xMinimum) xMinimum = point.x
|
||||
if (point.x > xMaximum) xMaximum = point.x
|
||||
if (point.y < yMinimum) yMinimum = point.y
|
||||
if (point.y > yMaximum) yMaximum = point.y
|
||||
if (includeErrors) {
|
||||
const errors = resolveWaveformPointErrors(point)
|
||||
minimum = Math.min(minimum, point.y - errors.lower)
|
||||
maximum = Math.max(maximum, point.y + errors.upper)
|
||||
if (errors.lower !== 0 || errors.upper !== 0) hasErrorPoints = true
|
||||
yMinimum = Math.min(yMinimum, point.y - errors.lower)
|
||||
yMaximum = Math.max(yMaximum, point.y + errors.upper)
|
||||
}
|
||||
})
|
||||
return paddedDomain(Number.isFinite(minimum) ? [minimum, maximum] : [])
|
||||
}
|
||||
return {
|
||||
xDomain: paddedDomain(Number.isFinite(xMinimum) ? [xMinimum, xMaximum] : []),
|
||||
yDomain: paddedDomain(Number.isFinite(yMinimum) ? [yMinimum, yMaximum] : []),
|
||||
hasErrorPoints,
|
||||
}
|
||||
}
|
||||
|
||||
export function prepareWaveformSeries(data: WaveformData): PreparedWaveformSeries[] {
|
||||
return normalizeWaveformSeries(data).map((series) => ({
|
||||
...series,
|
||||
xDomain: pointDomain(series.points, 'x'),
|
||||
yDomain: pointDomain(series.points, 'y', series.errorBar.visible),
|
||||
}))
|
||||
return normalizeWaveformSeries(data).map((series) => {
|
||||
const metrics = pointMetrics(series.points, series.errorBar.visible)
|
||||
return { ...series, ...metrics }
|
||||
})
|
||||
}
|
||||
|
||||
export function usePreparedWaveformSeries(data: () => WaveformData, onDataChange: () => void) {
|
||||
|
||||
@@ -30,7 +30,11 @@ export type {
|
||||
NormalizedWaveformSeries,
|
||||
} from '../../types'
|
||||
|
||||
export type { WaveformGridOptions } from '../core/grid'
|
||||
export type {
|
||||
WaveformGridOptions,
|
||||
WaveformGridLineOptions,
|
||||
WaveformGridTrackLines,
|
||||
} from '../core/grid'
|
||||
|
||||
// 重新导出数据处理函数
|
||||
export { normalizeWaveformData, normalizeWaveformSeries } from '../../core'
|
||||
|
||||
@@ -24,6 +24,8 @@ export type {
|
||||
WaveformPointType,
|
||||
WaveformErrorBarOptions,
|
||||
WaveformGridOptions,
|
||||
WaveformGridLineOptions,
|
||||
WaveformGridTrackLines,
|
||||
} from './data/types'
|
||||
|
||||
export type { WaveformGridOptions as WaveformGridConfig } from './core/grid'
|
||||
|
||||
@@ -211,42 +211,54 @@ watch(
|
||||
<g
|
||||
class="waveform-track__grid waveform-track__grid--minor waveform-chart__grid waveform-chart__grid--minor"
|
||||
>
|
||||
<line
|
||||
v-for="tick in track.xMinorTicks"
|
||||
:key="`x-minor-${track.index}-${tick}`"
|
||||
:x1="track.xScale(tick)"
|
||||
:x2="track.xScale(tick)"
|
||||
y1="0"
|
||||
:y2="track.height"
|
||||
/>
|
||||
<line
|
||||
v-for="tick in track.yMinorTicks"
|
||||
:key="`y-minor-${track.index}-${tick}`"
|
||||
x1="0"
|
||||
:x2="track.width ?? innerWidth"
|
||||
:y1="track.yScale(tick)"
|
||||
:y2="track.yScale(tick)"
|
||||
/>
|
||||
<template v-if="track.gridLines.vertical">
|
||||
<line
|
||||
v-for="tick in track.xMinorTicks"
|
||||
:key="`x-minor-${track.index}-${tick}`"
|
||||
data-grid-direction="vertical"
|
||||
:x1="track.xScale(tick)"
|
||||
:x2="track.xScale(tick)"
|
||||
y1="0"
|
||||
:y2="track.height"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="track.gridLines.horizontal">
|
||||
<line
|
||||
v-for="tick in track.yMinorTicks"
|
||||
:key="`y-minor-${track.index}-${tick}`"
|
||||
data-grid-direction="horizontal"
|
||||
x1="0"
|
||||
:x2="track.width ?? innerWidth"
|
||||
:y1="track.yScale(tick)"
|
||||
:y2="track.yScale(tick)"
|
||||
/>
|
||||
</template>
|
||||
</g>
|
||||
<g
|
||||
class="waveform-track__grid waveform-track__grid--major waveform-chart__grid waveform-chart__grid--major"
|
||||
>
|
||||
<line
|
||||
v-for="tick in track.xMajorTicks"
|
||||
:key="`x-major-${track.index}-${tick}`"
|
||||
:x1="track.xScale(tick)"
|
||||
:x2="track.xScale(tick)"
|
||||
y1="0"
|
||||
:y2="track.height"
|
||||
/>
|
||||
<line
|
||||
v-for="tick in track.yMajorTicks"
|
||||
:key="`y-major-${track.index}-${tick}`"
|
||||
x1="0"
|
||||
:x2="track.width ?? innerWidth"
|
||||
:y1="track.yScale(tick)"
|
||||
:y2="track.yScale(tick)"
|
||||
/>
|
||||
<template v-if="track.gridLines.vertical">
|
||||
<line
|
||||
v-for="tick in track.xMajorTicks"
|
||||
:key="`x-major-${track.index}-${tick}`"
|
||||
data-grid-direction="vertical"
|
||||
:x1="track.xScale(tick)"
|
||||
:x2="track.xScale(tick)"
|
||||
y1="0"
|
||||
:y2="track.height"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="track.gridLines.horizontal">
|
||||
<line
|
||||
v-for="tick in track.yMajorTicks"
|
||||
:key="`y-major-${track.index}-${tick}`"
|
||||
data-grid-direction="horizontal"
|
||||
x1="0"
|
||||
:x2="track.width ?? innerWidth"
|
||||
:y1="track.yScale(tick)"
|
||||
:y2="track.yScale(tick)"
|
||||
/>
|
||||
</template>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user