refactor(chart): enforce 400-line source limit
Some checks failed
Package component / package (push) Failing after 4m19s
Some checks failed
Package component / package (push) Failing after 4m19s
This commit is contained in:
372
src/components/core/useWaveformLayout.ts
Normal file
372
src/components/core/useWaveformLayout.ts
Normal file
@@ -0,0 +1,372 @@
|
||||
import { scaleLinear, type ZoomTransform } from 'd3'
|
||||
import { computed, type ComputedRef, type Ref, type ShallowRef } from 'vue'
|
||||
|
||||
import { resolveWaveformRenderingOptions } from '../../core'
|
||||
import { formatScientificAxisExponent, formatScientificAxisLabel, paddedDomain } from '../../utils'
|
||||
import {
|
||||
layoutAnnotations,
|
||||
type AnnotationSeriesInfo,
|
||||
type AnnotationTrackLayout,
|
||||
} from '../annotation'
|
||||
import {
|
||||
channelColors,
|
||||
margin,
|
||||
MINIMUM_PLOT_WIDTH,
|
||||
Y_AXIS_CHARACTER_WIDTH,
|
||||
Y_AXIS_LABEL_BAND_WIDTH,
|
||||
Y_AXIS_LABEL_GAP,
|
||||
Y_AXIS_OUTER_PADDING,
|
||||
Y_AXIS_TICK_PADDING,
|
||||
} from './constants'
|
||||
import {
|
||||
getGridGap,
|
||||
getPageCount,
|
||||
normalizeGridOptions,
|
||||
paginateSeries,
|
||||
resolveGridCellGeometry,
|
||||
X_AXIS_BAND,
|
||||
} from './grid'
|
||||
import { buildTrackLayouts, measureTrackYAxisClearance, Y_AXIS_EXPONENT_GAP } from './layout'
|
||||
import type { DisplaySeries, DisplayTrack, TrackLayout } from './types'
|
||||
import type { PreparedWaveformSeries } from './useWaveformData'
|
||||
import type { ResolvedWaveformChartProps } from './waveformChartTypes'
|
||||
import type { useWaveformAnnotationInteraction } from '../annotation'
|
||||
|
||||
interface LayoutContext {
|
||||
props: ResolvedWaveformChartProps
|
||||
preparedSeries: ShallowRef<PreparedWaveformSeries[]>
|
||||
currentPage: Ref<number>
|
||||
hiddenSeriesIdSet: ComputedRef<Set<string>>
|
||||
chartWidth: ComputedRef<number>
|
||||
innerHeight: ComputedRef<number>
|
||||
isCleanView: ComputedRef<boolean>
|
||||
sharedTransform: ShallowRef<ZoomTransform>
|
||||
independentTransforms: ShallowRef<ZoomTransform[]>
|
||||
sharedYDomains: Ref<Record<string, [number, number]>>
|
||||
independentYDomains: Ref<Record<number, [number, number]>>
|
||||
annotationInteraction: ReturnType<typeof useWaveformAnnotationInteraction>
|
||||
}
|
||||
|
||||
export function useWaveformLayout(context: LayoutContext) {
|
||||
const {
|
||||
props,
|
||||
preparedSeries,
|
||||
currentPage,
|
||||
hiddenSeriesIdSet,
|
||||
chartWidth,
|
||||
innerHeight,
|
||||
isCleanView,
|
||||
sharedTransform,
|
||||
independentTransforms,
|
||||
sharedYDomains,
|
||||
independentYDomains,
|
||||
annotationInteraction,
|
||||
} = context
|
||||
const chartSeries = computed<DisplaySeries[]>(() =>
|
||||
preparedSeries.value.map((series, index): DisplaySeries => ({
|
||||
...series,
|
||||
color:
|
||||
series.color ??
|
||||
(index === 0 ? props.lineColor : channelColors[index % channelColors.length]),
|
||||
})),
|
||||
)
|
||||
const chartTracks = computed<DisplayTrack[]>(() => {
|
||||
const groupedSeries = new Map<string, DisplaySeries[]>()
|
||||
chartSeries.value.forEach((series) => {
|
||||
const trackId = series.trackId || series.id
|
||||
const trackSeries = groupedSeries.get(trackId)
|
||||
if (trackSeries) trackSeries.push(series)
|
||||
else groupedSeries.set(trackId, [series])
|
||||
})
|
||||
return Array.from(groupedSeries, ([id, series]) => {
|
||||
const visibleSeries = series.filter((item) => !hiddenSeriesIdSet.value.has(item.id))
|
||||
const xDomainValues: number[] = []
|
||||
const yDomainValues: number[] = []
|
||||
visibleSeries.forEach((item) => {
|
||||
xDomainValues.push(item.xDomain[0], item.xDomain[1])
|
||||
yDomainValues.push(item.yDomain[0], item.yDomain[1])
|
||||
})
|
||||
return {
|
||||
id,
|
||||
series,
|
||||
visibleSeries,
|
||||
xDomain: paddedDomain(xDomainValues),
|
||||
yDomain: paddedDomain(yDomainValues),
|
||||
}
|
||||
})
|
||||
})
|
||||
const gridOptions = computed(() => normalizeGridOptions(props.grid))
|
||||
const renderingOptions = computed(() => resolveWaveformRenderingOptions(props.rendering))
|
||||
const pageCount = computed(() => getPageCount(chartTracks.value.length, gridOptions.value))
|
||||
const pagedTracks = computed(() =>
|
||||
paginateSeries(chartTracks.value, currentPage.value, gridOptions.value),
|
||||
)
|
||||
const yAxisMetrics = computed(() => {
|
||||
const axisText = chartTracks.value
|
||||
.filter((track) => track.visibleSeries.length > 0)
|
||||
.map((track) => {
|
||||
const scale = scaleLinear(track.yDomain, [1, 0]).nice()
|
||||
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 = -(
|
||||
Y_AXIS_TICK_PADDING +
|
||||
tickTextWidth +
|
||||
exponentClearance +
|
||||
Y_AXIS_LABEL_GAP +
|
||||
Y_AXIS_LABEL_BAND_WIDTH / 2
|
||||
)
|
||||
const fullClearance =
|
||||
tickTextWidth +
|
||||
Y_AXIS_TICK_PADDING +
|
||||
exponentClearance +
|
||||
Y_AXIS_LABEL_GAP +
|
||||
Y_AXIS_LABEL_BAND_WIDTH +
|
||||
Y_AXIS_OUTER_PADDING
|
||||
return { tickClearance, fullClearance, labelCenterX }
|
||||
})
|
||||
const hasYAxisLabels = computed(() =>
|
||||
chartTracks.value.some(
|
||||
(track) =>
|
||||
track.visibleSeries.length === 1 &&
|
||||
Boolean(track.visibleSeries[0]?.name.trim() || props.yLabel),
|
||||
),
|
||||
)
|
||||
const hasVisibleWaveformData = computed(() =>
|
||||
chartTracks.value.some((track) => track.visibleSeries.length > 0),
|
||||
)
|
||||
const chartLeftMargin = computed(() =>
|
||||
Math.max(
|
||||
margin.left,
|
||||
hasYAxisLabels.value
|
||||
? yAxisMetrics.value.fullClearance
|
||||
: hasVisibleWaveformData.value
|
||||
? yAxisMetrics.value.tickClearance
|
||||
: 0,
|
||||
),
|
||||
)
|
||||
const multiAxisClearance = computed(() =>
|
||||
chartTracks.value.reduce(
|
||||
(maximum, track) => {
|
||||
const clearance = measureTrackYAxisClearance(track, props.overlayMode)
|
||||
return {
|
||||
left: Math.max(maximum.left, clearance.left),
|
||||
right: Math.max(maximum.right, clearance.right),
|
||||
}
|
||||
},
|
||||
{ left: 0, right: 0 },
|
||||
),
|
||||
)
|
||||
const resolvedChartLeftMargin = computed(() =>
|
||||
props.overlayMode === 'multi-axis'
|
||||
? Math.max(chartLeftMargin.value, multiAxisClearance.value.left)
|
||||
: chartLeftMargin.value,
|
||||
)
|
||||
const chartRightMargin = computed(() =>
|
||||
props.overlayMode === 'multi-axis'
|
||||
? Math.max(margin.right, multiAxisClearance.value.right)
|
||||
: margin.right,
|
||||
)
|
||||
const innerWidth = computed(() =>
|
||||
Math.max(0, chartWidth.value - resolvedChartLeftMargin.value - chartRightMargin.value),
|
||||
)
|
||||
const yAxisLayout = computed(() => {
|
||||
const baseGap = getGridGap(props.displayMode)
|
||||
const columnCount = gridOptions.value.columnCount
|
||||
const hasMultipleColumns = columnCount > 1
|
||||
const fullGap = Math.max(baseGap, yAxisMetrics.value.fullClearance)
|
||||
const tickGap = Math.max(baseGap, yAxisMetrics.value.tickClearance)
|
||||
const plotWidth = (innerWidth.value - fullGap * Math.max(0, columnCount - 1)) / columnCount
|
||||
const canReserveLabelClearance = plotWidth >= MINIMUM_PLOT_WIDTH
|
||||
return {
|
||||
horizontalGap:
|
||||
props.overlayMode === 'multi-axis' && hasMultipleColumns && hasVisibleWaveformData.value
|
||||
? Math.max(baseGap, multiAxisClearance.value.left + multiAxisClearance.value.right)
|
||||
: hasMultipleColumns && hasVisibleWaveformData.value
|
||||
? hasYAxisLabels.value && canReserveLabelClearance
|
||||
? fullGap
|
||||
: tickGap
|
||||
: baseGap,
|
||||
hideSecondaryLabels:
|
||||
props.overlayMode !== 'multi-axis' &&
|
||||
hasMultipleColumns &&
|
||||
hasYAxisLabels.value &&
|
||||
!canReserveLabelClearance,
|
||||
}
|
||||
})
|
||||
const hasWaveformData = computed(() => chartSeries.value.length > 0)
|
||||
const hasChartArea = computed(() => innerWidth.value > 0 && innerHeight.value > 0)
|
||||
const resolvedXLabel = computed(() => props.xLabel ?? `时间(${props.timeUnit})`)
|
||||
const activeInteractionMode = computed(() => props.interactionMode)
|
||||
const isZoomMode = computed(
|
||||
() => activeInteractionMode.value === 'zoom' || activeInteractionMode.value === undefined,
|
||||
)
|
||||
const sharedXDomain = computed(() => {
|
||||
const values: number[] = []
|
||||
chartTracks.value.forEach((track) => {
|
||||
if (track.visibleSeries.length) values.push(track.xDomain[0], track.xDomain[1])
|
||||
})
|
||||
return paddedDomain(values)
|
||||
})
|
||||
const initialXDomain = computed<[number, number]>(() => {
|
||||
const domain = props.initialXDomain
|
||||
if (
|
||||
domain &&
|
||||
Number.isFinite(domain[0]) &&
|
||||
Number.isFinite(domain[1]) &&
|
||||
domain[0] !== domain[1]
|
||||
) {
|
||||
return domain[0] < domain[1] ? domain : [domain[1], domain[0]]
|
||||
}
|
||||
return sharedXDomain.value
|
||||
})
|
||||
const resolveInitialTrackDomain = (track: TrackLayout): [number, number] => {
|
||||
const configuredDomain =
|
||||
props.initialXDomains?.[track.series.trackId ?? track.series.id] ??
|
||||
props.initialXDomains?.[track.series.id] ??
|
||||
props.initialXDomain
|
||||
if (
|
||||
configuredDomain &&
|
||||
Number.isFinite(configuredDomain[0]) &&
|
||||
Number.isFinite(configuredDomain[1]) &&
|
||||
configuredDomain[0] !== configuredDomain[1]
|
||||
) {
|
||||
return configuredDomain[0] < configuredDomain[1]
|
||||
? configuredDomain
|
||||
: [configuredDomain[1], configuredDomain[0]]
|
||||
}
|
||||
return paddedDomain(track.seriesList.flatMap((series) => series.xDomain))
|
||||
}
|
||||
const sharedZoomDomain = computed(
|
||||
() =>
|
||||
sharedTransform.value
|
||||
.rescaleX(scaleLinear(initialXDomain.value, [0, innerWidth.value]))
|
||||
.domain() as [number, number],
|
||||
)
|
||||
const gridCells = computed(() => {
|
||||
const cells = resolveGridCellGeometry(
|
||||
innerWidth.value,
|
||||
innerHeight.value,
|
||||
gridOptions.value,
|
||||
props.displayMode,
|
||||
pagedTracks.value.map(Boolean),
|
||||
yAxisLayout.value.horizontalGap,
|
||||
)
|
||||
return cells.map((cell, index) => ({ ...cell, series: pagedTracks.value[index] }))
|
||||
})
|
||||
const trackLayouts = computed<TrackLayout[]>(() =>
|
||||
buildTrackLayouts({
|
||||
cells: gridCells.value,
|
||||
grid: gridOptions.value,
|
||||
displayMode: props.displayMode,
|
||||
overlayMode: props.overlayMode,
|
||||
independentTransforms: independentTransforms.value,
|
||||
sharedZoomDomain: sharedZoomDomain.value,
|
||||
initialXDomain: props.initialXDomain ? initialXDomain.value : undefined,
|
||||
initialXDomains: props.initialXDomains,
|
||||
yDomains:
|
||||
props.displayMode === 'independent'
|
||||
? Object.fromEntries(
|
||||
chartTracks.value.flatMap((track, index) => {
|
||||
const domain = independentYDomains.value[index]
|
||||
return domain ? [[track.id, domain]] : []
|
||||
}),
|
||||
)
|
||||
: sharedYDomains.value,
|
||||
timeUnit: props.timeUnit,
|
||||
rendering: renderingOptions.value,
|
||||
hideSecondaryLabels: isCleanView.value || yAxisLayout.value.hideSecondaryLabels,
|
||||
yAxisLabelX: yAxisMetrics.value.labelCenterX,
|
||||
showCompactEmptyTracks: props.displayMode === 'compact' && hasWaveformData.value,
|
||||
}),
|
||||
)
|
||||
const annotationLayoutsForTrack = (track: TrackLayout): AnnotationTrackLayout[] =>
|
||||
track.seriesList.map((series) => ({
|
||||
...track,
|
||||
series,
|
||||
yScale:
|
||||
track.seriesPaths.find((seriesPath) => seriesPath.series.id === series.id)?.yScale ??
|
||||
track.yScale,
|
||||
}))
|
||||
const resolveSeriesYScale = (track: TrackLayout, seriesId: string) =>
|
||||
track.seriesPaths.find((seriesPath) => seriesPath.series.id === seriesId)?.yScale ??
|
||||
track.yScale
|
||||
const annotationTrackLayouts = computed<AnnotationTrackLayout[]>(() =>
|
||||
trackLayouts.value.flatMap(annotationLayoutsForTrack),
|
||||
)
|
||||
const renderedAnnotations = computed(() =>
|
||||
props.annotationsVisible
|
||||
? layoutAnnotations(
|
||||
props.annotations,
|
||||
annotationTrackLayouts.value,
|
||||
innerWidth.value,
|
||||
innerHeight.value,
|
||||
)
|
||||
: [],
|
||||
)
|
||||
const xAxisTitleY = computed(() => innerHeight.value + X_AXIS_BAND + 10)
|
||||
const editorSeries = computed<AnnotationSeriesInfo | undefined>(() => {
|
||||
const seriesId = annotationInteraction.editorDraft.value?.annotation.seriesId
|
||||
const series = chartSeries.value.find((item) => item.id === seriesId)
|
||||
return series
|
||||
? {
|
||||
id: series.id,
|
||||
name: series.name.trim() || series.id,
|
||||
color: series.color,
|
||||
unit: series.unit,
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
const resolveFrameNumber = (trackIndex: number): string | number | undefined => {
|
||||
if (props.frameNumber === undefined || props.frameNumber === null) return undefined
|
||||
if (chartTracks.value.length === 1) return props.frameNumber
|
||||
return typeof props.frameNumber === 'number'
|
||||
? props.frameNumber + trackIndex
|
||||
: `${props.frameNumber}-${trackIndex + 1}`
|
||||
}
|
||||
|
||||
return {
|
||||
chartSeries,
|
||||
chartTracks,
|
||||
gridOptions,
|
||||
pageCount,
|
||||
pagedTracks,
|
||||
resolvedChartLeftMargin,
|
||||
innerWidth,
|
||||
hasVisibleWaveformData,
|
||||
hasWaveformData,
|
||||
hasChartArea,
|
||||
resolvedXLabel,
|
||||
activeInteractionMode,
|
||||
isZoomMode,
|
||||
initialXDomain,
|
||||
sharedZoomDomain,
|
||||
resolveInitialTrackDomain,
|
||||
gridCells,
|
||||
trackLayouts,
|
||||
annotationLayoutsForTrack,
|
||||
resolveSeriesYScale,
|
||||
annotationTrackLayouts,
|
||||
renderedAnnotations,
|
||||
xAxisTitleY,
|
||||
editorSeries,
|
||||
resolveFrameNumber,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user