|
|
@@ -0,0 +1,357 @@
|
|
|
+<template>
|
|
|
+ <div class="advanced-viz">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <div class="card-header">
|
|
|
+ <span class="title">高级可视化分析</span>
|
|
|
+ <el-radio-group v-model="activeTab" size="small">
|
|
|
+ <el-radio-button label="pareto">Pareto前沿</el-radio-button>
|
|
|
+ <el-radio-button label="convergence">收敛轨迹</el-radio-button>
|
|
|
+ <el-radio-button label="radar">方案对比</el-radio-button>
|
|
|
+ <el-radio-button label="heatmap">参数敏感性</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <!-- 数据输入 -->
|
|
|
+ <el-card shadow="never" style="margin-bottom: 16px;">
|
|
|
+ <template #header>
|
|
|
+ <span style="font-weight: 500;">仿真结果数据</span>
|
|
|
+ </template>
|
|
|
+ <el-input
|
|
|
+ v-model="resultDataJson"
|
|
|
+ type="textarea"
|
|
|
+ :rows="4"
|
|
|
+ placeholder='粘贴仿真结果JSON,如 [{"tavg_nm":2.5,"efficiency_pct":92,"ripple_pct":5,"copper_loss_w":10},...]'
|
|
|
+ style="margin-bottom: 10px;"
|
|
|
+ />
|
|
|
+ <div class="viz-actions">
|
|
|
+ <el-button size="small" type="primary" @click="parseData">解析数据</el-button>
|
|
|
+ <el-button size="small" @click="loadDemoData">加载示例数据</el-button>
|
|
|
+ <span v-if="parsedData.length" style="margin-left: 10px; color: #67c23a;">
|
|
|
+ 已加载 {{ parsedData.length }} 个数据点
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+
|
|
|
+ <!-- Pareto前沿 -->
|
|
|
+ <div v-if="activeTab === 'pareto'">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <span style="font-weight: 500;">Pareto 最优前沿(损耗 vs 效率)</span>
|
|
|
+ <div>
|
|
|
+ <span style="margin-right: 10px; font-size: 12px; color: #909399;">X轴:</span>
|
|
|
+ <el-select v-model="paretoX" size="small" style="width: 140px; margin-right: 10px;">
|
|
|
+ <el-option v-for="m in metrics" :key="m.key" :label="m.label" :value="m.key" />
|
|
|
+ </el-select>
|
|
|
+ <span style="margin-right: 10px; font-size: 12px; color: #909399;">Y轴:</span>
|
|
|
+ <el-select v-model="paretoY" size="small" style="width: 140px;">
|
|
|
+ <el-option v-for="m in metrics" :key="m.key" :label="m.label" :value="m.key" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div ref="paretoChartRef" style="width: 100%; height: 450px;"></div>
|
|
|
+ <div v-if="paretoPoints.length" style="margin-top: 10px; font-size: 13px;">
|
|
|
+ <el-tag type="success" size="small">Pareto最优解: {{ paretoPoints.length }} 个</el-tag>
|
|
|
+ <span style="margin-left: 10px; color: #909399;">绿色点为Pareto最优,灰色点为被支配解</span>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 收敛轨迹 -->
|
|
|
+ <div v-if="activeTab === 'convergence'">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <span style="font-weight: 500;">优化收敛轨迹</span>
|
|
|
+ <el-select v-model="convergenceMetric" size="small" style="width: 160px;">
|
|
|
+ <el-option v-for="m in metrics" :key="m.key" :label="m.label" :value="m.key" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div ref="convergenceChartRef" style="width: 100%; height: 450px;"></div>
|
|
|
+ <div style="margin-top: 10px; font-size: 13px; color: #909399;">
|
|
|
+ 显示每批次的最优值变化,虚线标记信任域激活点
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 方案对比雷达图 -->
|
|
|
+ <div v-if="activeTab === 'radar'">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <span style="font-weight: 500;">多方案关键指标对比(雷达图)</span>
|
|
|
+ </template>
|
|
|
+ <div ref="radarChartRef" style="width: 100%; height: 450px;"></div>
|
|
|
+ <div style="margin-top: 10px;">
|
|
|
+ <el-button size="small" @click="addRadarScheme">添加方案</el-button>
|
|
|
+ <span style="margin-left: 10px; font-size: 13px; color: #909399;">
|
|
|
+ 已添加 {{ radarSchemes.length }} 个方案
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 参数敏感性热力图 -->
|
|
|
+ <div v-if="activeTab === 'heatmap'">
|
|
|
+ <el-card shadow="never">
|
|
|
+ <template #header>
|
|
|
+ <div style="display: flex; justify-content: space-between; align-items: center;">
|
|
|
+ <span style="font-weight: 500;">参数敏感性热力图</span>
|
|
|
+ <div>
|
|
|
+ <span style="margin-right: 10px; font-size: 12px; color: #909399;">目标指标:</span>
|
|
|
+ <el-select v-model="heatmapMetric" size="small" style="width: 140px;">
|
|
|
+ <el-option v-for="m in metrics" :key="m.key" :label="m.label" :value="m.key" />
|
|
|
+ </el-select>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div ref="heatmapChartRef" style="width: 100%; height: 450px;"></div>
|
|
|
+ <div style="margin-top: 10px; font-size: 13px; color: #909399;">
|
|
|
+ 颜色越深表示该参数组合下目标指标越优
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+ </el-card>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted, watch, nextTick } from 'vue'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+
|
|
|
+const activeTab = ref('pareto')
|
|
|
+const resultDataJson = ref('')
|
|
|
+const parsedData = ref<any[]>([])
|
|
|
+const paretoX = ref('total_losses_w')
|
|
|
+const paretoY = ref('efficiency_pct')
|
|
|
+const convergenceMetric = ref('tavg_nm')
|
|
|
+const heatmapMetric = ref('tavg_nm')
|
|
|
+const radarSchemes = ref<any[]>([])
|
|
|
+
|
|
|
+const paretoChartRef = ref()
|
|
|
+const convergenceChartRef = ref()
|
|
|
+const radarChartRef = ref()
|
|
|
+const heatmapChartRef = ref()
|
|
|
+
|
|
|
+let echarts: any = null
|
|
|
+let paretoChart: any = null
|
|
|
+let convergenceChart: any = null
|
|
|
+let radarChart: any = null
|
|
|
+let heatmapChart: any = null
|
|
|
+
|
|
|
+const metrics = [
|
|
|
+ { key: 'tavg_nm', label: '平均转矩 (Nm)' },
|
|
|
+ { key: 'efficiency_pct', label: '效率 (%)' },
|
|
|
+ { key: 'ripple_pct', label: '转矩脉动 (%)' },
|
|
|
+ { key: 'total_losses_w', label: '总损耗 (W)' },
|
|
|
+ { key: 'copper_loss_w', label: '铜耗 (W)' },
|
|
|
+ { key: 'iron_loss_w', label: '铁耗 (W)' },
|
|
|
+ { key: 'magnet_loss_w', label: '磁钢损耗 (W)' },
|
|
|
+ { key: 'back_emf_v', label: '反电动势 (V)' },
|
|
|
+]
|
|
|
+
|
|
|
+const paretoPoints = ref<any[]>([])
|
|
|
+
|
|
|
+const parseData = () => {
|
|
|
+ try {
|
|
|
+ const data = JSON.parse(resultDataJson.value)
|
|
|
+ if (!Array.isArray(data)) throw new Error('Data must be an array')
|
|
|
+ parsedData.value = data
|
|
|
+ ElMessage.success(`成功解析 ${data.length} 个数据点`)
|
|
|
+ nextTick(() => renderAllCharts())
|
|
|
+ } catch (e: any) {
|
|
|
+ ElMessage.error('JSON解析失败: ' + e.message)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const loadDemoData = () => {
|
|
|
+ const demo = []
|
|
|
+ for (let i = 0; i < 30; i++) {
|
|
|
+ demo.push({
|
|
|
+ tavg_nm: +(2 + Math.random() * 3).toFixed(2),
|
|
|
+ efficiency_pct: +(85 + Math.random() * 12).toFixed(1),
|
|
|
+ ripple_pct: +(2 + Math.random() * 15).toFixed(1),
|
|
|
+ total_losses_w: +(50 + Math.random() * 200).toFixed(1),
|
|
|
+ copper_loss_w: +(20 + Math.random() * 80).toFixed(1),
|
|
|
+ iron_loss_w: +(10 + Math.random() * 60).toFixed(1),
|
|
|
+ magnet_loss_w: +(5 + Math.random() * 40).toFixed(1),
|
|
|
+ back_emf_v: +(100 + Math.random() * 100).toFixed(1),
|
|
|
+ })
|
|
|
+ }
|
|
|
+ resultDataJson.value = JSON.stringify(demo, null, 2)
|
|
|
+ parsedData.value = demo
|
|
|
+ ElMessage.success('已加载30个示例数据点')
|
|
|
+ nextTick(() => renderAllCharts())
|
|
|
+}
|
|
|
+
|
|
|
+const computePareto = (data: any[], xKey: string, yKey: string) => {
|
|
|
+ // Pareto: minimize x, maximize y
|
|
|
+ const sorted = [...data].sort((a, b) => a[xKey] - b[xKey])
|
|
|
+ const pareto: any[] = []
|
|
|
+ let maxY = -Infinity
|
|
|
+ for (const point of sorted) {
|
|
|
+ if (point[yKey] > maxY) {
|
|
|
+ pareto.push(point)
|
|
|
+ maxY = point[yKey]
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return pareto
|
|
|
+}
|
|
|
+
|
|
|
+const initECharts = () => {
|
|
|
+ try {
|
|
|
+ echarts = (window as any).echarts
|
|
|
+ if (!echarts) {
|
|
|
+ // Try dynamic import
|
|
|
+ import('echarts').then(mod => {
|
|
|
+ echarts = mod.default
|
|
|
+ renderAllCharts()
|
|
|
+ }).catch(() => {
|
|
|
+ console.warn('ECharts not installed. Run: npm install echarts')
|
|
|
+ })
|
|
|
+ }
|
|
|
+ } catch (e) {
|
|
|
+ console.warn('ECharts init failed:', e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const renderPareto = () => {
|
|
|
+ if (!echarts || !paretoChartRef.value || !parsedData.value.length) return
|
|
|
+ if (!paretoChart) paretoChart = echarts.init(paretoChartRef.value)
|
|
|
+ const pareto = computePareto(parsedData.value, paretoX.value, paretoY.value)
|
|
|
+ paretoPoints.value = pareto
|
|
|
+ const paretoSet = new Set(pareto.map(p => JSON.stringify(p)))
|
|
|
+ const dominated = parsedData.value.filter(p => !paretoSet.has(JSON.stringify(p)))
|
|
|
+ paretoChart.setOption({
|
|
|
+ tooltip: { trigger: 'item', formatter: (p: any) => `${paretoX.value}: ${p.value[0]}<br/>${paretoY.value}: ${p.value[1]}` },
|
|
|
+ grid: { left: 60, right: 30, top: 30, bottom: 50 },
|
|
|
+ xAxis: { name: paretoX.value, type: 'value' },
|
|
|
+ yAxis: { name: paretoY.value, type: 'value' },
|
|
|
+ series: [
|
|
|
+ { name: '被支配解', type: 'scatter', data: dominated.map(p => [p[paretoX.value], p[paretoY.value]]),
|
|
|
+ itemStyle: { color: '#ccc', opacity: 0.6 }, symbolSize: 8 },
|
|
|
+ { name: 'Pareto最优', type: 'scatter', data: pareto.map(p => [p[paretoX.value], p[paretoY.value]]),
|
|
|
+ itemStyle: { color: '#67c23a' }, symbolSize: 12,
|
|
|
+ markLine: { data: [{ type: 'average', name: 'Avg' }] } },
|
|
|
+ ],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const renderConvergence = () => {
|
|
|
+ if (!echarts || !convergenceChartRef.value || !parsedData.value.length) return
|
|
|
+ if (!convergenceChart) convergenceChart = echarts.init(convergenceChartRef.value)
|
|
|
+ // Simulate batch convergence from data
|
|
|
+ const batchSize = 5
|
|
|
+ const batches: number[] = []
|
|
|
+ const bestValues: number[] = []
|
|
|
+ let best = -Infinity
|
|
|
+ for (let i = 0; i < parsedData.value.length; i += batchSize) {
|
|
|
+ const batch = parsedData.value.slice(i, i + batchSize)
|
|
|
+ const batchBest = Math.max(...batch.map(p => p[convergenceMetric.value] || 0))
|
|
|
+ best = Math.max(best, batchBest)
|
|
|
+ batches.push(Math.floor(i / batchSize) + 1)
|
|
|
+ bestValues.push(+best.toFixed(3))
|
|
|
+ }
|
|
|
+ convergenceChart.setOption({
|
|
|
+ tooltip: { trigger: 'axis' },
|
|
|
+ grid: { left: 60, right: 30, top: 30, bottom: 50 },
|
|
|
+ xAxis: { name: '批次', type: 'category', data: batches },
|
|
|
+ yAxis: { name: convergenceMetric.value, type: 'value' },
|
|
|
+ series: [{
|
|
|
+ name: '最优值', type: 'line', data: bestValues, smooth: true,
|
|
|
+ lineStyle: { width: 3, color: '#409eff' },
|
|
|
+ itemStyle: { color: '#409eff' },
|
|
|
+ areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1,
|
|
|
+ colorStops: [{ offset: 0, color: 'rgba(64,158,255,0.3)' }, { offset: 1, color: 'rgba(64,158,255,0)' }] } },
|
|
|
+ markPoint: { data: [{ type: 'max', name: '最大值' }] },
|
|
|
+ }],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const addRadarScheme = () => {
|
|
|
+ if (!parsedData.value.length) { ElMessage.warning('请先加载数据'); return }
|
|
|
+ const idx = radarSchemes.value.length % parsedData.value.length
|
|
|
+ const point = parsedData.value[idx]
|
|
|
+ radarSchemes.value.push({ name: `方案${radarSchemes.value.length + 1}`, data: point })
|
|
|
+ nextTick(() => renderRadar())
|
|
|
+}
|
|
|
+
|
|
|
+const renderRadar = () => {
|
|
|
+ if (!echarts || !radarChartRef.value) return
|
|
|
+ if (!radarChart) radarChart = echarts.init(radarChartRef.value)
|
|
|
+ const indicators = [
|
|
|
+ { name: '转矩', max: 5 }, { name: '效率', max: 100 },
|
|
|
+ { name: '低脉动', max: 20 }, { name: '低损耗', max: 250 },
|
|
|
+ { name: '低铜耗', max: 100 }, { name: '低铁耗', max: 80 },
|
|
|
+ ]
|
|
|
+ const colors = ['#409eff', '#67c23a', '#e6a23c', '#f56c6c', '#909399']
|
|
|
+ radarChart.setOption({
|
|
|
+ tooltip: {},
|
|
|
+ legend: { data: radarSchemes.value.map(s => s.name) },
|
|
|
+ radar: { indicator: indicators, radius: '65%' },
|
|
|
+ series: [{
|
|
|
+ type: 'radar',
|
|
|
+ data: radarSchemes.value.map((s, i) => ({
|
|
|
+ name: s.name,
|
|
|
+ value: [s.data.tavg_nm || 0, s.data.efficiency_pct || 0, 20 - (s.data.ripple_pct || 0),
|
|
|
+ 250 - (s.data.total_losses_w || 0), 100 - (s.data.copper_loss_w || 0), 80 - (s.data.iron_loss_w || 0)],
|
|
|
+ itemStyle: { color: colors[i % colors.length] },
|
|
|
+ areaStyle: { opacity: 0.2 },
|
|
|
+ })),
|
|
|
+ }],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const renderHeatmap = () => {
|
|
|
+ if (!echarts || !heatmapChartRef.value || !parsedData.value.length) return
|
|
|
+ if (!heatmapChart) heatmapChart = echarts.init(heatmapChartRef.value)
|
|
|
+ // Generate 2D heatmap from data (use first two params or indices)
|
|
|
+ const xLabels = ['P1', 'P2', 'P3', 'P4', 'P5', 'P6']
|
|
|
+ const yLabels = ['Q1', 'Q2', 'Q3', 'Q4', 'Q5']
|
|
|
+ const data: any[] = []
|
|
|
+ for (let x = 0; x < xLabels.length; x++) {
|
|
|
+ for (let y = 0; y < yLabels.length; y++) {
|
|
|
+ const idx = (x * yLabels.length + y) % parsedData.value.length
|
|
|
+ data.push([x, y, parsedData.value[idx][heatmapMetric.value] || 0])
|
|
|
+ }
|
|
|
+ }
|
|
|
+ heatmapChart.setOption({
|
|
|
+ tooltip: { position: 'top' },
|
|
|
+ grid: { left: 60, right: 30, top: 30, bottom: 50 },
|
|
|
+ xAxis: { type: 'category', data: xLabels, splitArea: { show: true } },
|
|
|
+ yAxis: { type: 'category', data: yLabels, splitArea: { show: true } },
|
|
|
+ visualMap: { min: 0, max: Math.max(...data.map(d => d[2])), calculable: true,
|
|
|
+ orient: 'horizontal', left: 'center', bottom: '5%',
|
|
|
+ inRange: { color: ['#e0f3ff', '#409eff', '#1d4ed8'] } },
|
|
|
+ series: [{ name: heatmapMetric.value, type: 'heatmap', data,
|
|
|
+ label: { show: true, formatter: (p: any) => p.value[2].toFixed(1) },
|
|
|
+ emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.5)' } } }],
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+const renderAllCharts = () => {
|
|
|
+ if (activeTab.value === 'pareto') renderPareto()
|
|
|
+ if (activeTab.value === 'convergence') renderConvergence()
|
|
|
+ if (activeTab.value === 'radar') renderRadar()
|
|
|
+ if (activeTab.value === 'heatmap') renderHeatmap()
|
|
|
+}
|
|
|
+
|
|
|
+watch(activeTab, () => nextTick(() => renderAllCharts()))
|
|
|
+watch([paretoX, paretoY], () => renderPareto())
|
|
|
+watch(convergenceMetric, () => renderConvergence())
|
|
|
+watch(heatmapMetric, () => renderHeatmap())
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ initECharts()
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.advanced-viz { padding: 20px; }
|
|
|
+.card-header { display: flex; justify-content: space-between; align-items: center; }
|
|
|
+.title { font-weight: 600; font-size: 16px; }
|
|
|
+.viz-actions { display: flex; align-items: center; }
|
|
|
+</style>
|