import { palette as C, SCORE_BANDS } from './data' const tipOpt = { backgroundColor: 'rgba(12,43,94,.94)', borderWidth: 0, textStyle: { color: '#fff', fontSize: 12 }, padding: [8, 12], extraCssText: 'box-shadow:0 6px 18px rgba(12,43,94,.3);border-radius:6px;', } function axisStyle(opts = {}) { return { axisLine: { lineStyle: { color: '#E4E9F2' } }, axisTick: { show: false }, axisLabel: { color: C.mut, fontSize: 11 }, splitLine: { lineStyle: { color: '#F2F5FA', type: 'dashed' } }, ...opts, } } // ===== 综合评分仪表盘 ===== export function scoreGaugeOption(current, previous) { const segColors = [ [SCORE_BANDS[0].hi / 10, SCORE_BANDS[0].axisColor], [SCORE_BANDS[1].hi / 10, SCORE_BANDS[1].axisColor], [SCORE_BANDS[2].hi / 10, SCORE_BANDS[2].axisColor], [SCORE_BANDS[3].hi / 10, SCORE_BANDS[3].axisColor], [1, SCORE_BANDS[4].axisColor], ] return { tooltip: { ...tipOpt, formatter: (p) => `${p.name}:${p.value.toFixed(2)} 分`, }, series: [ { type: 'gauge', startAngle: 215, endAngle: -35, min: 0, max: 10, radius: '88%', center: ['50%', '62%'], progress: { show: false }, axisLine: { lineStyle: { width: 24, color: segColors, shadowColor: 'rgba(0,0,0,0.05)', shadowBlur: 6, }, }, pointer: { length: '64%', width: 5, itemStyle: { color: C.ink }, }, anchor: { show: true, showAbove: true, size: 14, itemStyle: { borderColor: C.ink, borderWidth: 2, color: '#fff', }, }, axisTick: { distance: -24, length: 6, lineStyle: { color: '#fff', width: 1 }, }, splitLine: { distance: -28, length: 12, lineStyle: { color: '#fff', width: 2 }, }, axisLabel: { distance: -36, color: C.mut, fontSize: 10, formatter: (v) => v.toFixed(0), }, title: { show: false }, detail: { valueAnimation: true, formatter: (v) => `{val|${v.toFixed(2)}}{sub|分}`, rich: { val: { fontSize: 38, fontWeight: 700, color: C.ink, fontFamily: 'DIN, sans-serif' }, sub: { fontSize: 14, color: C.mut, padding: [0, 0, 0, 4] }, }, offsetCenter: [0, '72%'], }, data: [{ value: current, name: '综合评分' }], }, ], } } // ===== 一级类别综合得分(柱状对比 本期 vs 上期) ===== export function categoryBarOption(currList, prevList) { const cats = currList.map((c) => c.name) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'shadow' }, }, legend: { data: ['本期', '上期'], top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 12, }, grid: { top: 30, left: 8, right: 24, bottom: 28, containLabel: true }, xAxis: { type: 'category', data: cats, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 } }), }, yAxis: { type: 'value', min: 0, max: 10, interval: 2, ...axisStyle(), }, series: [ { name: '本期', type: 'bar', barWidth: 22, data: currList.map((c) => c.score.toFixed(2)), itemStyle: { color: C.brand, borderRadius: [3, 3, 0, 0] }, label: { show: true, position: 'top', color: C.ink, fontSize: 11, fontWeight: 600 }, }, { name: '上期', type: 'bar', barWidth: 22, data: prevList.map((c) => c.score.toFixed(2)), itemStyle: { color: C.faint, borderRadius: [3, 3, 0, 0], opacity: 0.6 }, label: { show: true, position: 'top', color: C.mut, fontSize: 11 }, }, ], } } // ===== 分项评分雷达图 ===== export function factorRadarOption(factors) { return { tooltip: { ...tipOpt, formatter: (p) => `${p.name}:${p.value.toFixed(2)} 分`, }, legend: { data: ['本期', '上期'], top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 12, }, radar: { indicator: factors.map((f) => ({ name: f.name, max: 10 })), center: ['50%', '56%'], radius: '68%', splitNumber: 5, axisName: { color: C.mut, fontSize: 11 }, splitLine: { lineStyle: { color: '#E4E9F2' } }, splitArea: { areaStyle: { color: ['#FAFBFE', '#fff'], }, }, axisLine: { lineStyle: { color: '#E4E9F2' } }, }, series: [ { type: 'radar', symbolSize: 4, emphasis: { areaStyle: { opacity: 0.4 } }, data: [ { name: '本期', value: factors.map((f) => f.current), lineStyle: { color: C.brand, width: 2 }, itemStyle: { color: C.brand }, areaStyle: { color: 'rgba(21,82,208,0.18)' }, }, { name: '上期', value: factors.map((f) => f.previous), lineStyle: { color: C.faint, width: 1.5, type: 'dashed' }, itemStyle: { color: C.faint }, areaStyle: { color: 'rgba(152,165,189,0.18)' }, }, ], }, ], } } // ===== 类别贡献度(横向条形图,按权重×得分贡献) ===== export function categoryContribOption(categoryWeighted) { const cats = categoryWeighted.map((c) => c.name) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'shadow' }, formatter: (p) => { const i = p[0].dataIndex const c = categoryWeighted[i] return `${c.name}
权重 ${c.weight.toFixed(2)}% · 加权贡献 ${(c.score * c.weight / 100).toFixed(3)}` }, }, grid: { top: 16, left: 8, right: 24, bottom: 28, containLabel: true }, xAxis: { type: 'value', min: 0, max: 10, ...axisStyle(), }, yAxis: { type: 'category', data: cats, ...axisStyle({ axisLabel: { color: C.ink, fontSize: 11.5 } }), }, series: [ { type: 'bar', barWidth: 18, data: categoryWeighted.map((c) => c.score.toFixed(2)), itemStyle: { color: (params) => categoryWeighted[params.dataIndex].color, borderRadius: [0, 4, 4, 0], }, label: { show: true, position: 'right', color: C.ink, fontSize: 11, fontWeight: 600 }, markLine: { symbol: ['none', 'none'], data: [ { xAxis: 4, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '中性下沿 4', color: C.mut, fontSize: 10 } }, { xAxis: 6, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '中性上沿 6', color: C.mut, fontSize: 10 } }, ], }, }, ], } } // ===== 4.2.2 新增板块 ===== // ===== 双系列 营收/利润同比 ===== export function revenueProfitLineOption(series) { const dates = series.map((s) => s[0]) const rev = series.map((s) => s[1]) const pro = series.map((s) => s[2]) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => `${v.toFixed(2)}%`, }, legend: { data: [ { name: '营收同比', itemStyle: { color: C.brand } }, { name: '利润同比', itemStyle: { color: C.up } }, ], top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: { type: 'value', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) }, series: [ { name: '营收同比', type: 'line', data: rev, symbol: 'none', smooth: true, lineStyle: { color: C.brand, width: 2.2 } }, { name: '利润同比', type: 'line', data: pro, symbol: 'none', smooth: true, lineStyle: { color: C.up, width: 2.2 } }, ], } } // ===== PE / PB 估值带 ===== function priceBandOption(series, opts, closeSeries) { const dates = series.map((s) => s[0]) const mid = series.map((s) => s[1]) const up = series.map((s) => s[2]) const dn = series.map((s) => s[3]) const cur = series.map((s) => s[4]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [ { name: '当前', itemStyle: { color: opts.curColor } }, { name: '上轨', itemStyle: { color: C.faint } }, { name: '中枢', itemStyle: { color: C.mut } }, { name: '下轨', itemStyle: { color: C.faint } }, ] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => v.toFixed(opts.toFixed || 2), }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: hasClose ? [ { type: 'value', name: opts.toFixed === 3 ? 'PB' : 'PE', nameTextStyle: { color: opts.curColor, fontSize: 10.5, padding: [0, 0, 0, -28] }, position: 'left', ...axisStyle() }, { type: 'value', name: '指数走势', nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, -28, 0, 0] }, position: 'right', ...axisStyle({ splitLine: { show: false } }) }, ] : { type: 'value', ...axisStyle() }, series: [ { name: '下轨', type: 'line', data: dn, symbol: 'none', smooth: true, lineStyle: { color: C.faint, type: 'dashed', width: 1 }, z: 1 }, { name: '上轨', type: 'line', data: up, symbol: 'none', smooth: true, lineStyle: { color: C.faint, type: 'dashed', width: 1 }, z: 1 }, { name: '中枢', type: 'line', data: mid, symbol: 'none', smooth: true, lineStyle: { color: C.mut, width: 1.2 }, z: 2, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(152,165,189,0.10)' }, { offset: 1, color: 'rgba(152,165,189,0.02)' }, ], }, }, }, { name: '当前', type: 'line', data: cur, symbol: 'none', smooth: true, lineStyle: { color: opts.curColor, width: 2.4 }, z: 3, markPoint: { symbol: 'circle', symbolSize: 8, itemStyle: { color: opts.curColor, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(opts.toFixed || 2), color: opts.curColor, fontSize: 10.5, position: 'top', fontWeight: 600 }, data: [{ name: '当前', value: cur[cur.length - 1], xAxis: dates[dates.length - 1], yAxis: cur[cur.length - 1] }], }, }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 1.6 }, z: 4, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } } export function peBandOption(series, closeSeries) { return priceBandOption(series, { curColor: C.brand, toFixed: 2 }, closeSeries) } export function pbBandOption(series, closeSeries) { return priceBandOption(series, { curColor: C.brandD, toFixed: 3 }, closeSeries) } // ===== 股息率 ===== export function dividendLineOption(series, closeSeries) { const dates = series.map((s) => s[0]) const values = series.map((s) => s[1]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [{ name: '股息率', itemStyle: { color: C.warn } }] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.brand } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => `${v.toFixed(2)}%`, }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: hasClose ? [ { type: 'value', name: '股息率', nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, 0, 0, -28] }, position: 'left', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) }, { type: 'value', name: '指数走势', nameTextStyle: { color: C.brand, fontSize: 10.5, padding: [0, -28, 0, 0] }, position: 'right', ...axisStyle({ splitLine: { show: false } }) }, ] : { type: 'value', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) }, series: [ { name: '股息率', type: 'line', data: values, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 2.4 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(232,145,45,0.28)' }, { offset: 1, color: 'rgba(232,145,45,0.02)' }, ], }, }, markPoint: { symbol: 'circle', symbolSize: 8, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => `${p.value.toFixed(2)}%`, color: C.warn, fontSize: 10.5, position: 'top', fontWeight: 600 }, data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }], }, }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.brand, width: 1.6 }, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.brand, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.brand, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } } // ===== ERP ===== export function erpLineOption(series, closeSeries) { const dates = series.map((s) => s[0]) const values = series.map((s) => s[1]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [{ name: 'ERP', itemStyle: { color: C.brandD } }] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => `${v.toFixed(2)}%`, }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: hasClose ? [ { type: 'value', name: 'ERP', nameTextStyle: { color: C.brandD, fontSize: 10.5, padding: [0, 0, 0, -28] }, position: 'left', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) }, { type: 'value', name: '指数走势', nameTextStyle: { color: C.warn, fontSize: 10.5, padding: [0, -28, 0, 0] }, position: 'right', ...axisStyle({ splitLine: { show: false } }) }, ] : { type: 'value', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' } }) }, series: [ { name: 'ERP', type: 'line', data: values, symbol: 'none', smooth: true, lineStyle: { color: C.brandD, width: 2.4 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(14,58,150,0.28)' }, { offset: 1, color: 'rgba(14,58,150,0.02)' }, ], }, }, // markLine: { // symbol: ['none', 'none'], // data: [ // { yAxis: 4.0, lineStyle: { color: C.mut, type: 'dashed', width: 1 }, label: { show: true, position: 'end', formatter: '历史中枢', color: C.mut, fontSize: 10 } }, // ], // }, markPoint: { symbol: 'circle', symbolSize: 8, itemStyle: { color: C.brandD, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => `${p.value.toFixed(2)}%`, color: C.brandD, fontSize: 10.5, position: 'top', fontWeight: 600 }, data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }], }, }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 1.6 }, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } } // ===== 换手率 + 成交额 双轴 ===== export function liquidityBarLineOption(series, closeSeries) { const dates = series.map((s) => s[0]) const turnover = series.map((s) => s[1]) const volume = series.map((s) => s[2]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [ { name: '换手率', itemStyle: { color: C.brand } }, { name: '成交额(千亿港元)', itemStyle: { color: C.up } }, ] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'shadow' } }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: 60, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: [ { type: 'value', name: '换手率(%)', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11, formatter: '{value}%' }, nameTextStyle: { color: C.mut, fontSize: 10 } }) }, { type: 'value', name: hasClose ? '成交额(千亿) / 指数走势' : '成交额(千亿)', ...axisStyle({ axisLabel: { color: C.mut, fontSize: 11 }, nameTextStyle: { color: C.mut, fontSize: 10 } }) }, ], series: [ { name: '换手率', type: 'bar', data: turnover, barWidth: 12, itemStyle: { color: 'rgba(21,82,208,0.65)', borderRadius: [3, 3, 0, 0] } }, { name: '成交额(千亿港元)', type: 'line', yAxisIndex: 1, data: volume, symbol: 'none', smooth: true, lineStyle: { color: C.up, width: 2.2 } }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 1.6 }, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } } // ===== RSI 单线 ===== export function rsiLineOption(series, closeSeries) { const dates = series.map((s) => s[0]) const values = series.map((s) => s[1]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [{ name: 'RSI', itemStyle: { color: C.brand } }] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => v.toFixed(2) }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: hasClose ? [ { type: 'value', min: 0, max: 100, interval: 25, ...axisStyle() }, { type: 'value', name: '指数走势', position: 'right', ...axisStyle({ splitLine: { show: false }, nameTextStyle: { color: C.warn, fontSize: 10 } }) }, ] : { type: 'value', min: 0, max: 100, interval: 25, ...axisStyle() }, series: [ { name: 'RSI', type: 'line', data: values, symbol: 'none', smooth: true, lineStyle: { color: C.brand, width: 2.4 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(21,82,208,0.28)' }, { offset: 1, color: 'rgba(21,82,208,0.02)' }, ], }, }, markLine: { symbol: ['none', 'none'], data: [ { yAxis: 70, lineStyle: { color: C.up, type: 'dashed' }, label: { show: true, position: 'end', formatter: '超买 70', color: C.up, fontSize: 10 } }, { yAxis: 30, lineStyle: { color: C.down, type: 'dashed' }, label: { show: true, position: 'end', formatter: '超卖 30', color: C.down, fontSize: 10 } }, { yAxis: 50, lineStyle: { color: C.mut, type: 'dotted' }, label: { show: true, position: 'end', formatter: '中枢 50', color: C.mut, fontSize: 10 } }, ], }, markPoint: { symbol: 'circle', symbolSize: 8, itemStyle: { color: C.brand, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(2), color: C.brand, fontSize: 10.5, position: 'top', fontWeight: 600 }, data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }], }, }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 1.6 }, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } } // ===== VOSC 单线 ===== export function voscLineOption(series, closeSeries) { const dates = series.map((s) => s[0]) const values = series.map((s) => s[1]) const hasClose = Array.isArray(closeSeries) && closeSeries.length > 0 const closeMap = hasClose ? new Map(closeSeries.map(([d, v]) => [d, v])) : null const closeAligned = hasClose ? dates.map((d) => (closeMap.has(d) ? closeMap.get(d) : null)) : [] const lastClose = hasClose ? closeSeries[closeSeries.length - 1][1] : null const lastCloseDate = hasClose ? closeSeries[closeSeries.length - 1][0] : null const legendData = [{ name: 'VOSC', itemStyle: { color: C.up } }] if (hasClose) legendData.push({ name: '指数走势', itemStyle: { color: C.warn } }) return { tooltip: { ...tipOpt, trigger: 'axis', axisPointer: { type: 'line' }, valueFormatter: (v) => v.toFixed(3) }, legend: { data: legendData, top: 4, textStyle: { color: C.mut, fontSize: 11 }, itemWidth: 14, }, grid: { top: 36, left: 50, right: hasClose ? 60 : 24, bottom: 32, containLabel: true }, xAxis: { type: 'category', data: dates, ...axisStyle({ axisLabel: { color: C.mut, fontSize: 10.5 } }) }, yAxis: hasClose ? [ { type: 'value', ...axisStyle() }, { type: 'value', name: '指数走势', position: 'right', ...axisStyle({ splitLine: { show: false }, nameTextStyle: { color: C.warn, fontSize: 10 } }) }, ] : { type: 'value', ...axisStyle() }, series: [ { name: 'VOSC', type: 'line', data: values, symbol: 'none', smooth: true, lineStyle: { color: C.up, width: 2.4 }, areaStyle: { color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: 'rgba(225,59,59,0.25)' }, { offset: 1, color: 'rgba(225,59,59,0.02)' }, ], }, }, // markLine: { // symbol: ['none', 'none'], // data: [{ yAxis: 0, lineStyle: { color: C.mut, type: 'dashed' }, label: { show: true, position: 'end', formatter: '零轴', color: C.mut, fontSize: 10 } }], // }, markPoint: { symbol: 'circle', symbolSize: 8, itemStyle: { color: C.up, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(3), color: C.up, fontSize: 10.5, position: 'top', fontWeight: 600 }, data: [{ name: '当前', value: values[values.length - 1], xAxis: dates[dates.length - 1], yAxis: values[values.length - 1] }], }, }, ...(hasClose ? [{ name: '指数走势', type: 'line', yAxisIndex: 1, data: closeAligned, symbol: 'none', smooth: true, lineStyle: { color: C.warn, width: 1.6 }, markPoint: { symbol: 'circle', symbolSize: 7, itemStyle: { color: C.warn, borderColor: '#fff', borderWidth: 2 }, label: { formatter: (p) => p.value.toFixed(0), color: C.warn, fontSize: 10, position: 'bottom', fontWeight: 600 }, data: [{ name: '当前指数走势', value: lastClose, xAxis: lastCloseDate, yAxis: lastClose }], }, }] : []), ], } }