|
|
<template> <div ref="bottomRadarRef" id="bottomRadarChart"></div> </template>
<script setup> import { ref, onBeforeUnmount } from 'vue' import { createChartResizeHandler, applyResponsiveStyles } from '@/utils/chartResize.js' import * as echarts from 'echarts' const bottomRadarRef = ref(null) let bottomRadarChart = null let resizeHandler = null function initEmotionalBottomRadar(KlineData, barAndLineData) { // 如果已存在图表实例,先销毁
if (bottomRadarChart) { bottomRadarChart.dispose() bottomRadarChart = null } let bottomRadarChartDom = document.getElementById('bottomRadarChart') bottomRadarChart = echarts.init(bottomRadarChartDom)
// 日期-作为x轴
let dateArray = barAndLineData.map(subArray => subArray[0]) // k线数据格式:['2025/04/24', 250.5, 259.51, 249.2, 259.54, 274.899, 0.685, 258.354]
// 原始数据:索引1=开盘价, 索引2=收盘价, 索引3=最低价, 索引4=最高价
// ECharts candlestick需要:[开盘, 收盘, 最低, 最高]
let kLineDataArray = KlineData.map(subArray => [ subArray[1], // 开盘价
subArray[2], // 收盘价
subArray[3], // 最低价
subArray[4] // 最高价
]) // 计算K线数据的最小值,用于设置y轴起始值
let allKlineValues = [] kLineDataArray.forEach(item => { if (Array.isArray(item) && item.length >= 4) { // K线数据格式:[开盘价, 收盘价, 最低价, 最高价]
allKlineValues.push(item[0], item[1], item[2], item[3]) } }) // 找到最小值和最大值
let validValues = allKlineValues.filter(val => typeof val === 'number' && !isNaN(val)) let minValue = Math.min(...validValues) let maxValue = Math.max(...validValues) // 最小值向下取整,最大值向上取整
let yAxisMin = Math.floor(minValue) let yAxisMax = Math.ceil(maxValue) // 红线,取第二个值
let redLineDataArray = barAndLineData.map(subArray => subArray[1]) // 色块数据格式化
let barTotalDataArray = barAndLineData.map(subArray => subArray.slice(2, 6)) // 删掉
// barTotalDataArray[0] = [0, 0, 0, 0]
// barTotalDataArray[8] = [1, 1, 1, 1]
// barTotalDataArray[9] = [0, 0, 1, 1]
// barTotalDataArray[13] = [1, 0, 1, 0]
// barTotalDataArray[16] = [0, 0, 1, 0]
// 黄色块、黄色加仓资金、紫色柱子、蓝色柱子
let yellowBlockDataArray = [] let yellowLineDataArray = [] let purpleLineDataArray = [] let blueLineDataArray = [] let transparentFillingDataArray = [] // 黄色块:为1 0-4显示柱体
// 黄色加仓资金文字:为1 在4的位置展示文字
// 紫色柱子:为1 1-80显示柱体
// 蓝色柱子:为1 0-40显示柱体
// 因数据要互相叠加展示,所以需要数据处理。base适用于 ECharts 4.x 及以上版本
barTotalDataArray.forEach((item) => { if (item[0]) { yellowBlockDataArray.push(4) if (item[3]) { // 40-4
blueLineDataArray.push(36) if (item[2]) { // 80-40
purpleLineDataArray.push(40) transparentFillingDataArray.push(0) } else { purpleLineDataArray.push(0) transparentFillingDataArray.push(0) } } else { blueLineDataArray.push(0) if (item[2]) { // 80-4
purpleLineDataArray.push(76) transparentFillingDataArray.push(0) } else { purpleLineDataArray.push(0) transparentFillingDataArray.push(0) } } } else if (!item[0]) { yellowBlockDataArray.push(0) if (item[3]) { blueLineDataArray.push(40) if (item[2]) { // 80-40
purpleLineDataArray.push(40) transparentFillingDataArray.push(0) } else { purpleLineDataArray.push(0) transparentFillingDataArray.push(0) } } else { blueLineDataArray.push(0) if (item[2]) { // 80-1,base为1
purpleLineDataArray.push(79) transparentFillingDataArray.push(1) } else { purpleLineDataArray.push(0) transparentFillingDataArray.push(0) } } } // 加仓资金
if (item[1]) { yellowLineDataArray.push(1) } else if (!item[1]) { yellowLineDataArray.push(0) } })
// 配置图表选项,很多操作和展示已限制,如果需要需放开
let option = { // backgroundColor: '#000046', // 设置整个图表的背景色
tooltip: { show: true, // 启用tooltip显示
trigger: 'axis', triggerOn: 'mousemove', confine: true, axisPointer: { type: 'cross', crossStyle: { color: '#fff', width: 1, type: 'solid' }, lineStyle: { color: '#fff', width: 1, type: 'solid' }, label: { backgroundColor: 'rgba(0, 0, 0, 0.8)', color: '#fff', borderColor: '#fff', borderWidth: 1 } }, backgroundColor: 'rgba(0, 0, 0, 0.8)', borderColor: '#fff', borderWidth: 1, padding: 10, textStyle: { color: '#fff', fontSize: 12 }, formatter: function (params) { if (!params || params.length === 0) return '' // 检查是否有第二个或第三个网格的数据,如果有则不显示tooltip
let hasSecondOrThirdGrid = params.some(param => { return (param.seriesName === '红线' && param.axisIndex === 1) || (param.axisIndex === 2) || (param.seriesName !== 'K线' && param.seriesName !== '基础base') }) // 如果鼠标悬浮在第二个或第三个网格上,不显示tooltip
if (hasSecondOrThirdGrid && !params.some(param => param.seriesType === 'candlestick')) { return '' } let result = `<div style="font-weight: bold; color: #fff; margin-bottom: 8px;">${params[0].name}</div>`
params.forEach(param => { let value = param.value let color = param.color
if (param.seriesType === 'candlestick') { // ECharts candlestick的value格式:[开盘, 收盘, 最低, 最高]
let candlestickData = param.value // 确保数据有效性
if (!Array.isArray(candlestickData) || candlestickData.length < 4) { return '' } let openPrice = candlestickData[1] // 开盘价
let closePrice = candlestickData[2] // 收盘价
let lowPrice = candlestickData[3] // 最低价
let highPrice = candlestickData[4] // 最高价
// 确保所有价格都是有效数字
if (typeof openPrice !== 'number' || typeof closePrice !== 'number' || typeof lowPrice !== 'number' || typeof highPrice !== 'number') { return '' } let priceChange = closePrice - openPrice let changePercent = ((priceChange / openPrice) * 100).toFixed(2) let changeColor = priceChange >= 0 ? '#14b143' : '#ef232a' // 互换颜色:上涨红色,下跌绿色
result += `<div style="margin-bottom: 6px;">` result += `<div style="color: #fff; font-weight: bold;">${param.seriesName}</div>` result += `<div style="color: #fff;">开盘: ${openPrice.toFixed(1)}</div>` result += `<div style="color: #fff;">收盘: ${closePrice.toFixed(1)}</div>` result += `<div style="color: #fff;">最低: ${lowPrice.toFixed(1)}</div>` result += `<div style="color: #fff;">最高: ${highPrice.toFixed(1)}</div>` result += `<div style="color: ${changeColor};">涨跌: ${priceChange >= 0 ? '+' : ''}${priceChange.toFixed(2)} (${changePercent}%)</div>` result += `</div>` } else if (param.seriesName === '红线') { result += `<div style="color: #ef232a; margin-bottom: 4px;">${param.seriesName}: ${value}</div>` } else if (param.seriesName !== '基础base' && value > 0) { result += `<div style="color: ${color}; margin-bottom: 4px;">${param.seriesName}: ${value}</div>` } }) return result } }, legend: { // data: ['K线', '红线', '色块'], 不要展示图例
type: 'scroll', pageButtonItemGap: 2, pageButtonPosition: 'end', textStyle: { color: '#666' } }, grid: [ { left: '10%', right: '3%', top: '20px', bottom: '50%', height: '300px', width: '85%' // containLabel: true
}, { left: '10%', right: '3%', top: '320px', bottom: '25%', height: '300px', width: '85%'
// containLabel: true
}, { left: '10%', right: '3%', top: '620px', bottom: '50px', height: '300px', width: '85%'
// containLabel: true
} ], xAxis: [ { type: 'category', data: dateArray, gridIndex: 0, boundaryGap: true, // 保持间距,不要离y轴太近,不然重叠了
axisLine: { // show: false,
lineStyle: { color: 'white', // x轴线颜色
} }, axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false // 不要x轴的分割线
}, axisPointer: { link: { xAxisIndex: 'all' }, } }, { type: 'category', data: dateArray, gridIndex: 1, boundaryGap: true, axisLine: { // show: false,
lineStyle: { // color: '#008000'
color: 'white' } }, axisTick: { show: false }, axisLabel: { show: false }, splitLine: { show: false }, axisPointer: { link: { xAxisIndex: 'all' } } }, { type: 'category', data: dateArray, gridIndex: 2, axisLine: { lineStyle: { color: 'white' } }, axisTick: { show: true, // 显示刻度线
alignWithLabel: true, lineStyle: { color: '#999', // 颜色
width: 1, // 宽度
type: 'solid' // 线样式(solid/dashed/dotted)
} }, axisLabel: { color: 'white', interval: 'auto', // 自动计算显示间隔,只显示部分日期但覆盖所有范围
rotate: 0 // 取消倾斜角度
}, splitLine: { show: false }, axisPointer: { link: { xAxisIndex: 'all' } } } ], yAxis: [ { type: 'value', gridIndex: 0, splitNumber: 4, min: yAxisMin, // 设置y轴最小值为数据最小值向下取整
max: yAxisMax, // 设置y轴最大值为数据最大值向上取整
axisLine: { lineStyle: { color: 'white' // y轴坐标轴颜色
} }, axisTick: { show: true }, axisLabel: { width: 50, // 宽度限制
color: 'white', formatter: function (value, index) { return value.toFixed(2) } }, splitLine: { show: false, lineStyle: { color: '#837b7b', type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
} }, scale: true, // 不强制包含0,不然k线图底部空余太多
}, { type: 'value', gridIndex: 1, splitNumber: 3, axisLine: { lineStyle: { color: 'white' } }, axisTick: { show: true }, splitNumber: 5, // 刻度数量
axisLabel: { width: 50, // 宽度限制
color: 'white', formatter: function (value, index) { // 如果没有刻度数量,其他方法获取不到y轴刻度总长
if (index === 0) { return '0' } else if (index === 5) { return '' } return value } }, splitLine: { show: false, lineStyle: { color: '#837b7b', type: 'dotted' } }, }, { type: 'value', gridIndex: 2, splitNumber: 2, axisLine: { lineStyle: { color: 'white' } }, axisTick: { show: true }, splitNumber: 5, // 刻度数量
axisLabel: { width: 50, // 宽度限制
color: 'white', formatter: function (value, index) { if (index === 5) { return '' } return value } }, splitLine: { show: false, lineStyle: { color: '#837b7b', type: 'dotted' // 设置网格线类型 dotted:虚线 solid:实线
} }, splitNumber: 5, min: function (value) { return 0 // 最小值
}, max: function (value) { return value.max + 10 // 比最大值高10, 避免最高点和上一个图表x轴重合
} } ], dataZoom: [ { type: 'slider', xAxisIndex: [0, 1, 2], start: 0, end: 100, show: true, bottom: window.innerWidth > 768 ? 80 : 50, height: 20, borderColor: '#CFD6E3', fillerColor: 'rgba(135, 175, 274, 0.2)', handleStyle: { color: '#CFD6E3' }, textStyle: { color: '#fff' }, dataBackground: { lineStyle: { color: '#CFD6E3' }, areaStyle: { color: 'rgba(241,243,247,0.5)' } } }, { type: 'inside', xAxisIndex: [0, 1, 2], start: 0, end: 100, zoomOnMouseWheel: true, moveOnMouseMove: true, moveOnMouseWheel: false } ], series: [ { name: 'K线', type: 'candlestick', data: kLineDataArray, xAxisIndex: 0, yAxisIndex: 0, itemStyle: { color: '#14b143', // 开盘价 > 收盘价时为绿色
color0: '#ef232a', // 开盘价 < 收盘价时为红色
borderColor: '#14b143', borderColor0: '#ef232a', normal: { color: '#14b143', // 开盘价 > 收盘价时为绿色
color0: '#ef232a', // 开盘价 < 收盘价时为红色
borderColor: '#14b143', borderColor0: '#ef232a', opacity: function (params) { // K线数据格式:[开,收,低,高] 收盘价 > 开盘价时为阳线,设置边框不透明、填充透明
return params.data[1] > params.data[0] ? 0 : 1 } } } }, { name: '红线', type: 'line', data: redLineDataArray, xAxisIndex: 1, yAxisIndex: 1, symbol: 'none', sampling: 'average', itemStyle: { normal: { color: '#ef232a' } }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(33, 150, 243, 0.4)' }, { offset: 1, color: 'rgba(33, 150, 243, 0)' } ] } } }, { name: '基础base', type: 'bar', stack: 'total', // barGap: '-100%', // 重叠
xAxisIndex: 2, yAxisIndex: 2, barCategoryGap: '0%', itemStyle: { normal: { color: '#ffffff', borderWidth: 0, } }, data: transparentFillingDataArray, }, { name: '黄色', type: 'bar', stack: 'total', // barGap: '-100%', // 重叠
xAxisIndex: 2, yAxisIndex: 2, barCategoryGap: '0%', // 类目间柱条间距为0
itemStyle: { normal: { color: 'rgba(255, 255, 0, 1)', borderWidth: 0, // 加仓资金的文字显示
label: { show: (params) => { return yellowLineDataArray[params.dataIndex] > 0 }, position: 'top', textStyle: { color: 'rgba(255, 255, 0, 1)' }, formatter: (params) => { return yellowLineDataArray[params.dataIndex] > 0 ? '加仓资金' : '' } } } }, data: yellowBlockDataArray, }, { name: '蓝色', type: 'bar', stack: 'total', xAxisIndex: 2, yAxisIndex: 2, barCategoryGap: '0%', // 类目间柱条间距为0
label: { show: true, position: 'inside' }, itemStyle: { normal: { color: 'rgba(34, 196, 190, 1)', borderWidth: 0 } }, data: blueLineDataArray }, { name: '紫色', type: 'bar', stack: 'total', xAxisIndex: 2, yAxisIndex: 2, barCategoryGap: '0%', // 类目间柱条间距为0
label: { show: true, position: 'inside' }, itemStyle: { normal: { color: 'rgba(191, 87, 222, 1)', borderWidth: 0 } }, data: purpleLineDataArray }, ] }
// 使用配置项显示图表
bottomRadarChart.setOption(option)
// 应用响应式样式
if (bottomRadarRef.value) { applyResponsiveStyles(bottomRadarRef.value); } // 创建响应式处理器
if (resizeHandler) { resizeHandler.cleanup(); } resizeHandler = createChartResizeHandler({ chart: bottomRadarChart, container: bottomRadarRef.value, option: option, beforeResize: adjustGridHeight, name: '情绪探底雷达图表' }); // 立即触发一次resize以确保初始布局正确
setTimeout(() => { if (resizeHandler) { resizeHandler.triggerResize(); } }, 100); function adjustGridHeight() { if (window.innerWidth <= 768) { option.grid[0].height = '150px' option.grid[1].height = '150px' option.grid[2].height = '150px' option.grid[0].left = '15%' option.grid[1].left = '15%' option.grid[2].left = '15%' option.grid[1].top = '170px' option.grid[2].top = '320px' option.grid[0].width = '80%' option.grid[1].width = '80%' option.grid[2].width = '80%' } bottomRadarChart.setOption(option) }
// 初始化时调整高度
adjustGridHeight() }
// 暴露给父级
defineExpose({ initEmotionalBottomRadar })
onBeforeUnmount(() => { // 清理响应式处理器
if (resizeHandler) { resizeHandler.cleanup(); resizeHandler = null; } // 组件卸载时销毁图表
if (bottomRadarChart) { bottomRadarChart.dispose(); bottomRadarChart = null; } }) </script>
<style> #bottomRadarChart { width: 100%; height: 1040px; box-sizing: border-box; overflow: hidden; margin: 0px auto !important; padding: 0; }
/* 手机端适配样式 */ @media only screen and (max-width: 768px) { #bottomRadarChart { width: 90% !important; height: 560px; padding: 0; } } </style>
|