Ver código fonte

数据查询:归一合并改为归一化分图(各子图独立归一、tooltip显示归一值+原值);PKS 0值在图中断开且不参与归一;模型1标签改数据查询

18922397810 2 semanas atrás
pai
commit
76a2430a8f
3 arquivos alterados com 130 adições e 207 exclusões
  1. 3 3
      frontend/src/App.vue
  2. 127 202
      frontend/src/components/WaveChart.vue
  3. 0 2
      frontend/src/styles.css

+ 3 - 3
frontend/src/App.vue

@@ -35,7 +35,7 @@ const referencePoints = ref<TimePoint[]>([])
 const faults = ref<Fault[]>([])
 const startIndex = ref(0)
 const waveData = ref<WaveWindowResponse | null>(null)
-const chartMode = ref<'split' | 'merge'>('split')
+const chartMode = ref<'split' | 'normalized'>('split')
 const activeTab = ref<'model1' | 'model2' | 'alarm'>('model1')
 const chartDirty = ref(false)
 const queryLoading = ref(false)
@@ -904,7 +904,7 @@ onBeforeUnmount(() => {
     </header>
 
     <el-tabs v-model="activeTab" class="workspace-tabs">
-      <el-tab-pane label="模型1" name="model1">
+      <el-tab-pane label="数据查询" name="model1">
         <main class="workspace">
       <section class="query-panel panel">
         <div class="panel-heading compact-heading">
@@ -1224,7 +1224,7 @@ onBeforeUnmount(() => {
             <span class="annotation-width">标度宽度:<strong>{{ annotationWidth }}</strong></span>
             <el-radio-group v-model="chartMode" class="mode-switch" size="small">
               <el-radio-button label="split">分图显示</el-radio-button>
-              <el-radio-button label="merge">归一合并</el-radio-button>
+              <el-radio-button label="normalized">归一化分图</el-radio-button>
             </el-radio-group>
             <el-switch
               v-model="noSampling"

+ 127 - 202
frontend/src/components/WaveChart.vue

@@ -8,7 +8,7 @@ import { fixedAxisRange } from '../utils/axis'
 const props = defineProps<{
   data: WaveWindowResponse | null
   points: TimePoint[]
-  mode: 'split' | 'merge'
+  mode: 'split' | 'normalized'
   loading?: boolean
   annotations?: Annotation[]
   siteDescriptions?: Record<string, string>
@@ -78,7 +78,6 @@ const SPLIT_ROW_HEIGHT = 120
 const SPLIT_ROW_GAP = 14
 const CHART_TOP_INSET = 32
 const CHART_BOTTOM_INSET = 68
-const MERGE_CHART_HEIGHT = 560
 const EMPTY_CHART_HEIGHT = 480
 
 function buildRowMetas(data: WaveWindowResponse): RowMeta[] {
@@ -122,7 +121,7 @@ function rowColor(data: WaveWindowResponse, name: string): string {
 }
 
 function fixedYAxis(devicePoint: string) {
-  if (props.mode === 'merge') return null
+  if (props.mode === 'normalized') return null
   const series = props.data?.series.find((item) => item.devicePoint === devicePoint)
   if (!series) return null
   return fixedAxisRange(series.measurementType, series.min, series.max)
@@ -133,7 +132,6 @@ function splitExtraCount(data: WaveWindowResponse): number {
 }
 
 const layoutHeightPx = computed(() => {
-  if (props.mode === 'merge') return props.data ? MERGE_CHART_HEIGHT : EMPTY_CHART_HEIGHT
   if (!props.data) return EMPTY_CHART_HEIGHT
   return splitTotalHeight(buildRowMetas(props.data).length)
 })
@@ -239,7 +237,7 @@ function annotationAreas(annotations: Annotation[]): any[] {
 function buildAnnotationOverlaySeries(annotations: Annotation[]): echarts.SeriesOption[] {
   const data = props.data
   if (!data) return []
-  const gridCount = props.mode === 'merge' ? 1 : buildRowMetas(data).length
+  const gridCount = buildRowMetas(data).length
   const areas = annotationAreas(annotations)
   return Array.from({ length: gridCount }, (_, index) => ({
     id: `annotation-overlay-${index}`,
@@ -322,19 +320,14 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
     ? data.cycles.filter((cycle) => cycle.background)
     : primaryCycles
   const periodBackground = periodAreas(backgroundCycles)
-  const showSecond = data.secondSeries.data.length > 0
-  const showVolume = data.volumeSeries.data.length > 0 || data.volumeSeries.info != null
   const siteSeries = data.siteSeries?.series.filter((series) => series.data.length) ?? []
-  const showSite = siteSeries.length > 0
-  const metas = props.mode === 'merge' ? [] : buildRowMetas(data)
-  const rows = props.mode === 'merge' ? ['合并信号'] : metas.map((meta) => meta.name)
+  const metas = buildRowMetas(data)
+  const rows = metas.map((meta) => meta.name)
   const gridCount = rows.length
   const topInset = CHART_TOP_INSET
   const bottomInset = CHART_BOTTOM_INSET
-  const rowGap = props.mode === 'merge' ? 0 : SPLIT_ROW_GAP
-  const rowHeight = props.mode === 'merge'
-    ? Math.max(260, MERGE_CHART_HEIGHT - topInset - bottomInset)
-    : SPLIT_ROW_HEIGHT
+  const rowGap = SPLIT_ROW_GAP
+  const rowHeight = SPLIT_ROW_HEIGHT
   const grid = Array.from({ length: gridCount }, (_, index) => ({
     left: 70,
     right: 22,
@@ -342,9 +335,7 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
     height: rowHeight,
     containLabel: false,
   }))
-  if (props.mode === 'merge') {
-    grid[0] = { left: 70, right: 22, top: topInset, height: rowHeight, containLabel: false }
-  }
+  const normalized = props.mode === 'normalized'
   const xAxes = grid.map((_, index) => ({
     type: 'value' as const,
     min: data.xMin,
@@ -373,7 +364,7 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
     let axisName = rowName
     if (meta?.kind === 'site') {
       const rule = siteScaleRuleFor(props.siteDescriptions?.[rowName])
-      if (rule) axisName = `${rowName} ${rule.unit}`
+      if (rule && !normalized) axisName = `${rowName} ${rule.unit}`
     } else if (meta?.kind === 'siteAgg') {
       siteRange = { min: 0, max: 1, interval: 0.2 }
     }
@@ -391,201 +382,141 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
         formatter: meta?.kind === 'siteAgg' ? (value: number) => Number(value).toFixed(1) : undefined,
       },
       splitLine: { show: true, lineStyle: { color: '#e7edf0', width: 1 } },
-      scale: meta?.kind === 'site' ? true : isDisplacement,
-      min: siteRange?.min ?? fixed?.min,
-      max: siteRange?.max ?? fixed?.max,
-      interval: siteRange?.interval ?? fixed?.interval,
+      scale: normalized ? false : (meta?.kind === 'site' ? true : isDisplacement),
+      min: normalized ? 0 : (siteRange?.min ?? fixed?.min),
+      max: normalized ? 1 : (siteRange?.max ?? fixed?.max),
+      interval: normalized ? 0.2 : (siteRange?.interval ?? fixed?.interval),
     }
   })
   const series: echarts.SeriesOption[] = []
   const deviceColor = (devicePoint: string) => data.series.find((series) => series.devicePoint === devicePoint)?.color ?? colors[data.series.find((series) => series.devicePoint === devicePoint)?.measurementType ?? '加速度']
-  if (props.mode === 'merge') {
-    devicePoints.forEach((devicePoint) => {
-      const source = data.series.find((series) => series.devicePoint === devicePoint)
-      const values = source?.data ?? []
-      const finiteValues = values.map((item) => item.rawValue).filter(Number.isFinite)
-      const [safeMin, safeMax] = finiteValues.length ? minMaxOf(finiteValues) : [0, 1]
-      const span = safeMax - safeMin || 1
+  const normValue = (value: number, lo: number, hi: number) => (hi > lo ? (value - lo) / (hi - lo) : 0.5)
+  metas.forEach((meta, index) => {
+    if (meta.kind === 'device') {
+      const source = data.series.find((series) => series.devicePoint === meta.name)
       const color = source?.color ?? colors[source?.measurementType ?? '加速度']
+      const [lo, hi] = minMaxOf((source?.data ?? []).map((item) => item.rawValue).filter(Number.isFinite))
       series.push({
-        name: devicePoint,
-        type: 'line' as const,
+        name: meta.name,
+        type: 'line',
         z: 10,
-        xAxisIndex: 0,
-        yAxisIndex: 0,
+        xAxisIndex: index,
+        yAxisIndex: index,
         showSymbol: false,
         connectNulls: false,
         sampling: 'lttb' as const,
-        lineStyle: { width: 2.5, color, cap: 'round' as const, join: 'round' as const },
+        lineStyle: { width: 2.5, color, cap: 'round', join: 'round' },
         itemStyle: { color },
-        data: values
+        data: (source?.data ?? [])
           .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.rawValue))
-          .map((item) => [item.x, (item.rawValue - safeMin) / span] as [number, number]),
+          .map((item): number[] => (normalized
+            ? [item.x, normValue(item.rawValue, lo, hi), item.rawValue]
+            : [item.x, item.rawValue])),
         markArea: { silent: true, data: periodBackground },
+        markLine: index === 0 ? { silent: true, symbol: 'none', data: triggerLines(data.triggerXs) } : undefined,
       })
-    })
-    if (showSecond) {
-      const secondValues = data.secondSeries.data
-      const secondFiniteValues = secondValues.map((item) => item.rawValue).filter(Number.isFinite)
-      const [secondMin, secondMax] = secondFiniteValues.length ? minMaxOf(secondFiniteValues) : [0, 1]
-      const secondSpan = secondMax - secondMin || 1
+    } else if (meta.kind === 'site') {
+      const siteItem = siteSeries.find((series) => series.itemName === meta.name)
+      if (!siteItem) return
+      const color = SITE_POINT_COLORS[meta.siteSeq % SITE_POINT_COLORS.length]
+      const [lo, hi] = minMaxOf(
+        siteItem.data.map((item) => item.value[1]).filter((value) => Number.isFinite(value) && value !== 0),
+      )
       series.push({
-        name: '周期数据',
+        name: meta.name,
         type: 'line',
-        xAxisIndex: 0,
-        yAxisIndex: 0,
+        z: 10,
+        xAxisIndex: index,
+        yAxisIndex: index,
         showSymbol: false,
         connectNulls: false,
-        lineStyle: { width: 3, color: colors['周期数据'], cap: 'round' as const, join: 'round' as const },
-        areaStyle: { color: 'rgba(245, 108, 108, 0.16)' },
-        data: secondValues
-          .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.rawValue))
-          .map((item) => [item.x, (item.rawValue - secondMin) / secondSpan] as [number, number]),
-      })
-    }
-    if (showVolume) {
-      const volumeValues = data.volumeSeries.data
-      const volumeFinite = volumeValues.map((item) => item.volume).filter(Number.isFinite)
-      const [volumeMin, volumeMax] = volumeFinite.length ? minMaxOf(volumeFinite) : [0, 1]
-      const volumeSpan = volumeMax - volumeMin || 1
-      series.push({
-        name: '体积',
-        type: 'line',
-        xAxisIndex: 0,
-        yAxisIndex: 0,
-        showSymbol: false,
-        lineStyle: { width: 1.5, color: colors['体积'], opacity: 0.9 },
-        data: volumeValues
-          .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.volume))
-          .map((item) => [item.x, (item.volume - volumeMin) / volumeSpan] as [number, number]),
-      })
-    }
-    if (showSite) {
-      const siteFinite = siteSeries.flatMap((series) => series.data.map((item) => item.value[1])).filter(Number.isFinite)
-      const [siteMin, siteMax] = siteFinite.length ? minMaxOf(siteFinite) : [0, 1]
-      const siteSpan = siteMax - siteMin || 1
-      siteSeries.forEach((siteItem, index) => {
-        const color = SITE_POINT_COLORS[index % SITE_POINT_COLORS.length]
-        series.push({
-          name: siteItem.itemName,
-          type: 'line',
-          xAxisIndex: 0,
-          yAxisIndex: 0,
-          showSymbol: false,
-          connectNulls: false,
-          sampling: 'lttb' as const,
-          lineStyle: { width: 1.4, color, opacity: 0.8 },
-          itemStyle: { color },
-          data: siteItem.data
-            .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.value[1]))
-            .map((item) => [item.x, (item.value[1] - siteMin) / siteSpan] as [number, number]),
-        })
+        sampling: 'lttb' as const,
+        lineStyle: { width: 1.6, color, opacity: 0.9 },
+        itemStyle: { color },
+        data: siteItem.data
+          .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.value[1]))
+          .map((item): (number | null)[] => {
+            if (item.value[1] === 0) return [item.x, null]
+            return normalized
+              ? [item.x, normValue(item.value[1], lo, hi), item.value[1]]
+              : [item.x, item.value[1]]
+          }),
+        markArea: { silent: true, data: periodBackground },
       })
-    }
-  } else {
-    metas.forEach((meta, index) => {
-      if (meta.kind === 'device') {
-        const source = data.series.find((series) => series.devicePoint === meta.name)
-        const color = source?.color ?? colors[source?.measurementType ?? '加速度']
-        series.push({
-          name: meta.name,
-          type: 'line',
-          z: 10,
-          xAxisIndex: index,
-          yAxisIndex: index,
-          showSymbol: false,
-          connectNulls: false,
-          sampling: 'lttb' as const,
-          lineStyle: { width: 2.5, color, cap: 'round', join: 'round' },
-          itemStyle: { color },
-          data: source?.data
-            .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.rawValue))
-            .map((item) => [item.x, item.rawValue] as [number, number]) ?? [],
-          markArea: { silent: true, data: periodBackground },
-          markLine: index === 0 ? { silent: true, symbol: 'none', data: triggerLines(data.triggerXs) } : undefined,
-        })
-      } else if (meta.kind === 'site') {
-        const siteItem = siteSeries.find((series) => series.itemName === meta.name)
-        if (!siteItem) return
-        const color = SITE_POINT_COLORS[meta.siteSeq % SITE_POINT_COLORS.length]
+    } else if (meta.kind === 'siteAgg') {
+      siteSeries.forEach((siteItem) => {
+        const siteValues = siteItem.data
+          .filter((item) => Number.isFinite(item.value[1]) && item.value[1] !== 0)
+          .map((item) => item.value[1])
+        if (!siteValues.length) return
+        const siteMin = Math.min(...siteValues)
+        const siteMax = Math.max(...siteValues)
+        const siteSpan = siteMax - siteMin
+        const siteIndex = siteSeries.findIndex((series) => series.itemName === siteItem.itemName)
+        const color = SITE_POINT_COLORS[siteIndex % SITE_POINT_COLORS.length]
         series.push({
-          name: meta.name,
+          name: `${SITE_AGG_PREFIX}${siteItem.itemName}`,
           type: 'line',
-          z: 10,
+          z: 9,
           xAxisIndex: index,
           yAxisIndex: index,
           showSymbol: false,
           connectNulls: false,
           sampling: 'lttb' as const,
-          lineStyle: { width: 1.6, color, opacity: 0.9 },
+          lineStyle: { width: 1.2, color, opacity: 0.85 },
           itemStyle: { color },
           data: siteItem.data
             .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.value[1]))
-            .map((item) => [item.x, item.value[1]] as [number, number]),
-          markArea: { silent: true, data: periodBackground },
-        })
-      } else if (meta.kind === 'siteAgg') {
-        siteSeries.forEach((siteItem) => {
-          const siteValues = siteItem.data
-            .filter((item) => Number.isFinite(item.value[1]))
-            .map((item) => item.value[1])
-          if (!siteValues.length) return
-          const siteMin = Math.min(...siteValues)
-          const siteMax = Math.max(...siteValues)
-          const siteSpan = siteMax - siteMin
-          const siteIndex = siteSeries.findIndex((series) => series.itemName === siteItem.itemName)
-          const color = SITE_POINT_COLORS[siteIndex % SITE_POINT_COLORS.length]
-          series.push({
-            name: `${SITE_AGG_PREFIX}${siteItem.itemName}`,
-            type: 'line',
-            z: 9,
-            xAxisIndex: index,
-            yAxisIndex: index,
-            showSymbol: false,
-            connectNulls: false,
-            sampling: 'lttb' as const,
-            lineStyle: { width: 1.2, color, opacity: 0.85 },
-            itemStyle: { color },
-            data: siteItem.data
-              .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.value[1]))
-              .map((item) => [item.x, siteSpan > 0 ? (item.value[1] - siteMin) / siteSpan : 0.5] as [number, number]),
-            markArea: { silent: true, data: periodBackground },
-          })
-        })
-      } else if (meta.kind === 'second') {
-        series.push({
-          name: '周期数据',
-          type: 'line',
-          z: 10,
-          xAxisIndex: index,
-          yAxisIndex: index,
-          showSymbol: false,
-          connectNulls: false,
-          lineStyle: { width: 3, color: colors['周期数据'], cap: 'round', join: 'round' },
-          areaStyle: { color: 'rgba(245, 108, 108, 0.16)' },
-          itemStyle: { color: colors['周期数据'] },
-          data: data.secondSeries.data
-            .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.rawValue))
-            .map((item) => [item.x, item.rawValue] as [number, number]),
+            .map((item): (number | null)[] => {
+              if (item.value[1] === 0) return [item.x, null]
+              return [
+                item.x,
+                siteSpan > 0 ? (item.value[1] - siteMin) / siteSpan : 0.5,
+                item.value[1],
+              ]
+            }),
           markArea: { silent: true, data: periodBackground },
         })
-      } else if (meta.kind === 'volume') {
-        series.push({
-          name: '体积',
-          type: 'line',
-          z: 10,
-          xAxisIndex: index,
-          yAxisIndex: index,
-          showSymbol: false,
-          lineStyle: { width: 2, color: colors['体积'] },
-          itemStyle: { color: colors['体积'] },
-          data: data.volumeSeries.data
-            .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.volume))
-            .map((item) => [item.x, item.volume] as [number, number]),
-        })
-      }
-    })
-  }
+      })
+    } else if (meta.kind === 'second') {
+      const [lo, hi] = minMaxOf(data.secondSeries.data.map((item) => item.rawValue).filter(Number.isFinite))
+      series.push({
+        name: '周期数据',
+        type: 'line',
+        z: 10,
+        xAxisIndex: index,
+        yAxisIndex: index,
+        showSymbol: false,
+        connectNulls: false,
+        lineStyle: { width: 3, color: colors['周期数据'], cap: 'round', join: 'round' },
+        areaStyle: { color: 'rgba(245, 108, 108, 0.16)' },
+        itemStyle: { color: colors['周期数据'] },
+        data: data.secondSeries.data
+          .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.rawValue))
+          .map((item): number[] => (normalized
+            ? [item.x, normValue(item.rawValue, lo, hi), item.rawValue]
+            : [item.x, item.rawValue])),
+        markArea: { silent: true, data: periodBackground },
+      })
+    } else if (meta.kind === 'volume') {
+      const [lo, hi] = minMaxOf(data.volumeSeries.data.map((item) => item.volume).filter(Number.isFinite))
+      series.push({
+        name: '体积',
+        type: 'line',
+        z: 10,
+        xAxisIndex: index,
+        yAxisIndex: index,
+        showSymbol: false,
+        lineStyle: { width: 2, color: colors['体积'] },
+        itemStyle: { color: colors['体积'] },
+        data: data.volumeSeries.data
+          .filter((item) => Number.isFinite(item.x) && Number.isFinite(item.volume))
+          .map((item): number[] => (normalized
+            ? [item.x, normValue(item.volume, lo, hi), item.volume]
+            : [item.x, item.volume])),
+      })
+    }
+  })
 
   const zoom = zoomMode === 'keep' ? readCurrentZoom(data) : initialZoom(data)
   series.push(...buildAnnotationOverlaySeries(props.annotations ?? []))
@@ -609,24 +540,26 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
         const lines = [`<strong>窗口位置:${Number(first.value?.[0] ?? 0).toFixed(4)}</strong>`]
         params.forEach((item) => {
           const value = item.value?.[1]
-          if (value !== undefined && value !== null) {
-            const fullName = String(item.seriesName ?? '')
-            const isAgg = fullName.startsWith(SITE_AGG_PREFIX)
-            const base = isAgg ? fullName.slice(SITE_AGG_PREFIX.length) : fullName
-            const seriesName = props.siteDescriptions?.[base] || base
+          if (value === undefined || value === null) return
+          const raw = item.value?.[2]
+          const fullName = String(item.seriesName ?? '')
+          const isAgg = fullName.startsWith(SITE_AGG_PREFIX)
+          const base = isAgg ? fullName.slice(SITE_AGG_PREFIX.length) : fullName
+          const seriesName = props.siteDescriptions?.[base] || base
+          const marker = `<span style="color:${item.color}">●</span> ${seriesName}`
+          if (raw !== undefined && raw !== null) {
             lines.push(
-              `<span style="color:${item.color}">●</span> ${seriesName}${isAgg ? ' (归一化)' : ''}: `
-              + `${Number(value).toPrecision(isAgg ? 3 : 7)}`,
+              `${marker}: 归一化 ${Number(value).toFixed(3)} / 原值 ${Number(raw).toPrecision(7)}`,
             )
+          } else {
+            lines.push(`${marker}${isAgg ? ' (归一化)' : ''}: ${Number(value).toPrecision(isAgg ? 3 : 7)}`)
           }
         })
         return lines.join('<br/>')
       },
     },
     legend: {
-      data: props.mode === 'merge'
-        ? [...devicePoints, ...siteSeries.map((series) => series.itemName), ...(showSecond ? ['周期数据'] : []), ...(showVolume ? ['体积'] : [])]
-        : metas.filter((meta) => meta.kind !== 'siteAgg').map((meta) => meta.name),
+      data: metas.filter((meta) => meta.kind !== 'siteAgg').map((meta) => meta.name),
       formatter: (name: string) => props.siteDescriptions?.[name] || name,
       top: 0,
       left: 70,
@@ -650,14 +583,6 @@ function buildOption(zoomMode: 'initial' | 'keep' = 'initial') {
         end: zoom.end,
       },
     ],
-    graphic: props.mode === 'merge' ? [
-      {
-        type: 'text',
-        left: 70,
-        top: 31,
-        style: { text: '归一化值', fill: '#8b9aa2', fontSize: 11 },
-      },
-    ] : [],
   }
 }
 
@@ -804,7 +729,7 @@ function onBlankDoubleClick(event: any) {
   const px = event.offsetX
   const py = event.offsetY
   if (typeof px !== 'number' || typeof py !== 'number') return
-  const gridCount = props.mode === 'merge' ? 1 : props.data.devicePoints.length + splitExtraCount(props.data) + 1
+  const gridCount = props.data.devicePoints.length + splitExtraCount(props.data) + 1
   let inGrid = false
   for (let index = 0; index < gridCount; index += 1) {
     if (chart.containPixel({ gridIndex: index }, [px, py])) {
@@ -896,7 +821,7 @@ onBeforeUnmount(() => {
         <canvas :ref="(el) => setPvCanvas(el, rowIndex)" class="pv-preview-canvas" :aria-label="row.title"></canvas>
       </div>
     </div>
-    <div ref="chartElement" class="wave-chart" :class="{ 'is-merge': mode === 'merge' }" :style="{ height: `${layoutHeightPx}px` }"></div>
+    <div ref="chartElement" class="wave-chart" :style="{ height: `${layoutHeightPx}px` }"></div>
     <div v-if="annotations?.length" class="annotation-nav">
       <div class="annotation-nav-head">
         <span class="annotation-nav-title">标注导航</span>

+ 0 - 2
frontend/src/styles.css

@@ -249,7 +249,6 @@ h2 { font-size: 18px; line-height: 1.35; font-weight: 600; }
 .chart-separator { width: 1px; height: 14px; margin: 0 3px; background: #dcdfe6; }
 .chart-point-count { margin-left: auto; color: #606266; font-weight: 500; }
 .wave-chart { width: 100%; height: 680px; }
-.wave-chart.is-merge { height: 560px; }
 .pv-preview-shell { display: flex; flex-direction: column; gap: 6px; width: 100%; padding: 4px 0; }
 .pv-preview-row { position: relative; width: 100%; height: 132px; overflow: hidden; border: 1px solid #ebeef5; border-radius: 4px; background: #f8fafb; }
 .pv-preview-title { position: absolute; z-index: 1; top: 3px; left: 10px; color: #8b9aa2; font-size: 11px; pointer-events: none; }
@@ -344,7 +343,6 @@ h2 { font-size: 18px; line-height: 1.35; font-weight: 600; }
   .chart-actions { width: 100%; align-items: flex-start; flex-direction: column; gap: 10px; }
   .chart-status { align-self: flex-end; }
   .wave-chart { height: 600px; }
-  .wave-chart.is-merge { height: 520px; }
   .pv-preview-row, .pv-preview-canvas { height: 118px; }
   .chart-toolbar-note { font-size: 11px; }
   .chart-point-count { display: none; }