ModelIterationVisualization.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512
  1. <script setup lang='ts'>
  2. import { ref, onMounted, nextTick, onUnmounted, watch } from 'vue';
  3. import VueEcharts from 'vue-echarts';
  4. import 'echarts';
  5. import { api5000 } from '../../../utils/request';
  6. import { useI18n } from 'vue-i18n';
  7. const { t } = useI18n();
  8. interface HistoryDataItem {
  9. dataset_id: number;
  10. row_count: number;
  11. model_id: number;
  12. model_name: string;
  13. performance_score: number;
  14. timestamp: string;
  15. }
  16. interface HistoryDataResponse {
  17. data_type: string;
  18. timestamps: string[];
  19. row_counts: number[];
  20. performance_scores: number[];
  21. model_details: HistoryDataItem[];
  22. }
  23. interface ScatterDataResponse {
  24. scatter_data: [number, number][];
  25. r2_score: number;
  26. y_range: [number, number];
  27. model_name: string;
  28. model_type: string;
  29. }
  30. const props = defineProps({
  31. showLineChart: {
  32. type: Boolean,
  33. default: true
  34. },
  35. showInitScatterChart: {
  36. type: Boolean,
  37. default: true
  38. },
  39. showMidScatterChart: {
  40. type: Boolean,
  41. default: true
  42. },
  43. showFinalScatterChart: {
  44. type: Boolean,
  45. default: true
  46. },
  47. lineChartPathParam: {
  48. type: String,
  49. default: 'reflux'
  50. },
  51. initScatterModelId: {
  52. type: Number,
  53. default: 6
  54. },
  55. midScatterModelId: {
  56. type: Number,
  57. default: 7
  58. },
  59. finalScatterModelId: {
  60. type: Number,
  61. default: 17
  62. }
  63. });
  64. // 定义响应式变量
  65. const ecLineOption = ref({});
  66. const ecInitScatterOption = ref({});
  67. const ecMidScatterOption = ref({});
  68. const ecFinalScatterOption = ref({});
  69. // 定义图表引用
  70. const ecLineOptionRef = ref<InstanceType<typeof VueEcharts>>();
  71. const ecInitScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
  72. const ecMidScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
  73. const ecFinalScatterOptionRef = ref<InstanceType<typeof VueEcharts>>();
  74. // 新增:图片URL和模型类型选择
  75. const learningCurveImageUrl = ref('');
  76. const dataIncreaseCurveImageUrl = ref('');
  77. const selectedModelType = ref('rf'); // 默认选择随机森林
  78. // 模型类型选项
  79. const modelTypeOptions = [
  80. { label: t('ModelIteration.randomForest'), value: 'rf' },
  81. { label: t('ModelIteration.xgboost'), value: 'xgbr' },
  82. { label: t('ModelIteration.gradientBoosting'), value: 'gbst' },
  83. ];
  84. // 计算数据范围的函数
  85. const calculateDataRange = (data: [number, number][]) => {
  86. const xValues = data.map(item => item[0]);
  87. const yValues = data.map(item => item[1]);
  88. return {
  89. xMin: Math.min(...xValues),
  90. xMax: Math.max(...xValues),
  91. yMin: Math.min(...yValues),
  92. yMax: Math.max(...yValues)
  93. };
  94. };
  95. // 获取折线图数据
  96. const fetchLineData = async () => {
  97. try {
  98. const response = await api5000.get<HistoryDataResponse>(`/get-model-history/${props.lineChartPathParam}`);
  99. const data = response.data;
  100. const timestamps = data.timestamps;
  101. const performanceScores: Record<string, number[]> = {};
  102. data.model_details.forEach((item: HistoryDataItem) => {
  103. const score = Number(item.performance_score);
  104. if (!performanceScores[item.model_name]) {
  105. performanceScores[item.model_name] = [];
  106. }
  107. performanceScores[item.model_name].push(score);
  108. });
  109. const series = Object.keys(performanceScores).map(modelName => ({
  110. name: modelName,
  111. type: 'line',
  112. data: performanceScores[modelName]
  113. }));
  114. ecLineOption.value = {
  115. tooltip: {
  116. trigger: 'axis'
  117. },
  118. legend: {
  119. data: Object.keys(performanceScores)
  120. },
  121. grid: {
  122. left: '3%',
  123. right: '17%',
  124. bottom: '3%',
  125. containLabel: true
  126. },
  127. xAxis: {
  128. name: t('ModelIteration.modelIteration'),
  129. type: 'category',
  130. boundaryGap: false,
  131. data: timestamps.map((_, index) => `${index + 1}代`)
  132. },
  133. yAxis: {
  134. name: 'Score (R^2)',
  135. type: 'value'
  136. },
  137. series
  138. };
  139. console.log('ecLineOption updated:', ecLineOption.value);
  140. } catch (error) {
  141. console.error('获取折线图数据失败:', error);
  142. }
  143. };
  144. // 获取散点图数据
  145. const fetchScatterData = async (modelId: number, optionRef: any) => {
  146. try {
  147. const response = await api5000.get<ScatterDataResponse>(`/model-scatter-data/${modelId}`);
  148. const data = response.data;
  149. const scatterData = data.scatter_data;
  150. const range = calculateDataRange(scatterData);
  151. const padding = 0.1;
  152. const xMin = range.xMin - Math.abs(range.xMin * padding);
  153. const xMax = range.xMax + Math.abs(range.xMax * padding);
  154. const yMin = range.yMin - Math.abs(range.yMin * padding);
  155. const yMax = range.yMax + Math.abs(range.yMax * padding);
  156. const min = Math.min(xMin, yMin);
  157. const max = Math.max(xMax, yMax);
  158. optionRef.value = {
  159. tooltip: {
  160. trigger: 'axis',
  161. axisPointer: {
  162. type: 'cross'
  163. }
  164. },
  165. legend: {
  166. data: ['True vs Predicted']
  167. },
  168. grid: {
  169. left: '3%',
  170. right: '22%',
  171. bottom: '3%',
  172. containLabel: true
  173. },
  174. xAxis: {
  175. name: 'True Values',
  176. type: 'value',
  177. min: min,
  178. max: max
  179. },
  180. yAxis: {
  181. name: 'Predicted Values',
  182. type: 'value',
  183. min: parseFloat(min.toFixed(2)),
  184. max: parseFloat(max.toFixed(2))
  185. },
  186. series: [
  187. {
  188. name: 'True vs Predicted',
  189. type: 'scatter',
  190. data: scatterData,
  191. symbolSize: 10,
  192. itemStyle: {
  193. color: '#1f77b4',
  194. opacity: 0.7
  195. }
  196. },
  197. {
  198. name: 'Trendline',
  199. type: 'line',
  200. data: [
  201. [min, min],
  202. [max, max]
  203. ],
  204. lineStyle: {
  205. type: 'dashed',
  206. color: '#ff7f0e',
  207. width: 2
  208. }
  209. }
  210. ]
  211. };
  212. } catch (error) {
  213. console.error('获取散点图数据失败:', error);
  214. }
  215. };
  216. // 新增:获取学习曲线图片
  217. const fetchLearningCurveImage = async () => {
  218. try {
  219. const response = await api5000.get('/latest-learning-curve', {
  220. params: {
  221. data_type: 'reflux',
  222. model_type: selectedModelType.value
  223. },
  224. responseType: 'blob'
  225. });
  226. const blob = new Blob([response.data], { type: 'image/png' });
  227. learningCurveImageUrl.value = URL.createObjectURL(blob);
  228. } catch (error) {
  229. console.error('获取学习曲线图片失败:', error);
  230. }
  231. };
  232. // 新增:获取数据增长曲线图片
  233. const fetchDataIncreaseCurveImage = async () => {
  234. try {
  235. const response = await api5000.get('/latest-data-increase-curve', {
  236. params: {
  237. data_type: 'reflux',
  238. },
  239. responseType: 'blob'
  240. });
  241. const blob = new Blob([response.data], { type: 'image/png' });
  242. dataIncreaseCurveImageUrl.value = URL.createObjectURL(blob);
  243. } catch (error) {
  244. console.error('获取数据增长曲线图片失败:', error);
  245. }
  246. };
  247. // 监听模型类型变化,重新获取图片
  248. watch(selectedModelType, () => {
  249. fetchLearningCurveImage();
  250. fetchDataIncreaseCurveImage();
  251. });
  252. // 定义调整图表大小的函数
  253. const resizeCharts = () => {
  254. nextTick(() => {
  255. if (props.showLineChart) ecLineOptionRef.value?.resize();
  256. if (props.showInitScatterChart) ecInitScatterOptionRef.value?.resize();
  257. if (props.showMidScatterChart) ecMidScatterOptionRef.value?.resize();
  258. if (props.showFinalScatterChart) ecFinalScatterOptionRef.value?.resize();
  259. });
  260. };
  261. onMounted(async () => {
  262. if (props.showLineChart) await fetchLineData();
  263. if (props.showInitScatterChart) await fetchScatterData(props.initScatterModelId, ecInitScatterOption);
  264. if (props.showMidScatterChart) await fetchScatterData(props.midScatterModelId, ecMidScatterOption);
  265. if (props.showFinalScatterChart) await fetchScatterData(props.finalScatterModelId, ecFinalScatterOption);
  266. // 新增:获取图片
  267. await fetchLearningCurveImage();
  268. await fetchDataIncreaseCurveImage();
  269. // 页面加载完成后调整图表大小
  270. resizeCharts();
  271. // 监听窗口大小变化,调整图表大小
  272. window.addEventListener('resize', resizeCharts);
  273. });
  274. // 组件卸载时移除事件监听器
  275. onUnmounted(() => {
  276. window.removeEventListener('resize', resizeCharts);
  277. // 清理Blob URL
  278. if (learningCurveImageUrl.value) {
  279. URL.revokeObjectURL(learningCurveImageUrl.value);
  280. }
  281. if (dataIncreaseCurveImageUrl.value) {
  282. URL.revokeObjectURL(dataIncreaseCurveImageUrl.value);
  283. }
  284. });
  285. </script>
  286. <template>
  287. <div class="container">
  288. <template v-if="showInitScatterChart">
  289. <!-- 散点图 -->
  290. <h2 class="chart-header">{{ $t('ModelIteration.scatterPlotTitle') }}</h2>
  291. <div class="chart-container">
  292. <VueEcharts :option="ecInitScatterOption" ref="ecInitScatterOptionRef" />
  293. </div>
  294. </template>
  295. <h2 class="chart-header">{{ $t('ModelIteration.modelPerformanceAnalysis') }}</h2>
  296. <!-- 模型性能分析部分 -->
  297. <div class="analysis-section">
  298. <!-- 数据增长曲线模块 -->
  299. <div class="chart-module">
  300. <h2 class="chart-header">{{ $t('ModelIteration.dataIncreaseCurve') }}</h2>
  301. <div class="image-chart-item">
  302. <div class="image-container">
  303. <img v-if="dataIncreaseCurveImageUrl" :src="dataIncreaseCurveImageUrl" :alt="$t('ModelIteration.dataIncreaseCurve')" >
  304. <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
  305. </div>
  306. </div>
  307. </div>
  308. <!-- 学习曲线模块 -->
  309. <div class="chart-module">
  310. <h2 class="chart-header">{{ $t('ModelIteration.learningCurve') }}</h2>
  311. <div class="model-selector-wrapper">
  312. <select v-model="selectedModelType" class="model-select">
  313. <option v-for="option in modelTypeOptions" :key="option.value" :value="option.value">
  314. {{ option.label }}
  315. </option>
  316. </select>
  317. </div>
  318. <div class="image-chart-item">
  319. <div class="image-container">
  320. <img v-if="learningCurveImageUrl" :src="learningCurveImageUrl" :alt="$t('ModelIteration.learningCurve')" >
  321. <div v-else class="image-placeholder">{{ $t('ModelIteration.loading') }}</div>
  322. </div>
  323. </div>
  324. </div>
  325. </div>
  326. </div>
  327. </template>
  328. <style scoped>
  329. .container {
  330. display: flex;
  331. flex-direction: column;
  332. align-items: center;
  333. justify-content: center;
  334. width: 100%;
  335. height: 100%;
  336. gap: 20px;
  337. color: #000;
  338. background-color: white;
  339. }
  340. .chart-header {
  341. font-size: 18px;
  342. font-weight: bold;
  343. margin-bottom: 10px;
  344. color: #2c3e50;
  345. }
  346. .sub-title {
  347. font-size: 14px;
  348. font-weight: 700;
  349. }
  350. .chart-container {
  351. width: 85%;
  352. height: 450px;
  353. margin: 0 auto;
  354. margin-bottom: 20px;
  355. background-color: white;
  356. border-radius: 8px;
  357. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  358. }
  359. .VueEcharts {
  360. width: 100%;
  361. height: 100%;
  362. margin: 0 10px;
  363. background-color: white;
  364. }
  365. /* 新增:图片图表样式 */
  366. .image-charts-section {
  367. width: 90%;
  368. margin: 30px auto;
  369. padding: 20px;
  370. background-color: #f8f9fa;
  371. border-radius: 8px;
  372. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  373. }
  374. .model-type-selector {
  375. margin: 20px 0;
  376. text-align: center;
  377. }
  378. .model-type-selector label {
  379. margin-right: 10px;
  380. font-weight: bold;
  381. color: #2c3e50;
  382. }
  383. .model-select {
  384. padding: 8px 12px;
  385. border: 1px solid #ddd;
  386. border-radius: 4px;
  387. background-color: white;
  388. font-size: 14px;
  389. }
  390. .image-charts-container {
  391. display: grid;
  392. grid-template-columns: 1fr 1fr;
  393. gap: 30px;
  394. margin-top: 20px;
  395. }
  396. .image-chart-item {
  397. background-color: white;
  398. padding: 20px;
  399. border-radius: 8px;
  400. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  401. }
  402. .image-chart-title {
  403. font-size: 16px;
  404. font-weight: bold;
  405. margin-bottom: 15px;
  406. color: #2c3e50;
  407. text-align: center;
  408. }
  409. .image-container {
  410. width: 100%;
  411. height: 400px;
  412. display: flex;
  413. align-items: center;
  414. justify-content: center;
  415. background-color: #f8f9fa;
  416. border-radius: 4px;
  417. overflow: hidden;
  418. }
  419. .image-container img {
  420. max-width: 100%;
  421. max-height: 100%;
  422. object-fit: contain;
  423. }
  424. .image-placeholder {
  425. color: #6c757d;
  426. font-style: italic;
  427. }
  428. /* 确保图表内部也是白色背景 */
  429. :deep(.echarts) {
  430. background-color: white;
  431. }
  432. /* 为图表标题添加样式 */
  433. :deep(.echarts-title) {
  434. color: #2c3e50;
  435. font-weight: bold;
  436. }
  437. /* 为坐标轴添加样式 */
  438. :deep(.echarts-axis) {
  439. color: #666;
  440. }
  441. /* 为图例添加样式 */
  442. :deep(.echarts-legend) {
  443. color: #333;
  444. }
  445. /* 响应式设计 */
  446. @media (max-width: 768px) {
  447. .image-charts-container {
  448. grid-template-columns: 1fr;
  449. }
  450. .image-container {
  451. height: 300px;
  452. }
  453. .chart-container {
  454. width: 95%;
  455. height: 400px;
  456. }
  457. }
  458. </style>