import type { BarSeriesOption, CandlestickSeriesOption, LineSeriesOption, } from "echarts/charts" import type { DataZoomComponentOption, GridComponentOption, LegendComponentOption, TitleComponentOption, TooltipComponentOption, } from "echarts/components" import type { ComposeOption } from "echarts/core" import type { SelectionChartPoint } from "../api/selection.types" const DEFAULT_VISIBLE_POINTS = 120 const zhixingLineSeries = [ { color: "#a8a8a8", key: "trend_white", name: "知行白线" }, { color: "#eab308", key: "trend_yellow", name: "知行黄线" }, ] as const export type SelectionChartOption = ComposeOption< | BarSeriesOption | CandlestickSeriesOption | DataZoomComponentOption | GridComponentOption | LegendComponentOption | LineSeriesOption | TitleComponentOption | TooltipComponentOption > /** * Build the three-grid financial chart configuration from aligned API points. * * @param points Ascending qfq OHLCV, J, and Zhixing-line points from the API. * @returns One ECharts option with synchronized axes and an exact 120-point window. */ export function buildSelectionChartOption( points: SelectionChartPoint[], ): SelectionChartOption { const dates = points.map((point) => point.trade_date) const startIndex = Math.max(0, dates.length - DEFAULT_VISIBLE_POINTS) const startValue = dates[startIndex] const endValue = dates.at(-1) const zoomRange = { endValue, startValue } return { animation: false, dataZoom: [ { ...zoomRange, type: "inside", xAxisIndex: [0, 1, 2], }, { ...zoomRange, bottom: 4, height: 20, type: "slider", xAxisIndex: [0, 1, 2], }, ], grid: [ { bottom: "46%", left: 64, right: 24, top: 36 }, { bottom: "28%", height: "14%", left: 64, right: 24 }, { bottom: 48, height: "20%", left: 64, right: 24 }, ], legend: [ { data: zhixingLineSeries.map((line) => line.name), right: 20, top: 8, }, { data: ["J"], right: 20, top: "73%", }, ], series: [ { data: points.map(toCandlestickValue), itemStyle: { borderColor: "#dc2626", borderColor0: "#16a34a", color: "#dc2626", color0: "#16a34a", }, name: "日线", type: "candlestick", xAxisIndex: 0, yAxisIndex: 0, }, ...zhixingLineSeries.map((line): LineSeriesOption => ({ connectNulls: false, data: points.map((point) => point[line.key]), itemStyle: { color: line.color }, lineStyle: { color: line.color, width: 1.5, }, name: line.name, showSymbol: false, smooth: false, type: "line", xAxisIndex: 0, yAxisIndex: 0, })), { data: points.map((point) => ({ itemStyle: { color: point.open !== null && point.close !== null && point.close >= point.open ? "#dc2626" : "#16a34a", }, value: point.volume, })), name: "成交量", type: "bar", xAxisIndex: 1, yAxisIndex: 1, }, { connectNulls: false, data: points.map((point) => point.j), lineStyle: { color: "#9333ea", width: 1.5, }, name: "J", showSymbol: false, smooth: false, type: "line", xAxisIndex: 2, yAxisIndex: 2, }, ], title: [ { left: 12, text: "日线", textStyle: { fontSize: 12 }, top: 8 }, { left: 72, text: "成交量", textStyle: { fontSize: 12 }, top: "54%" }, { left: 72, text: "J", textStyle: { fontSize: 12 }, top: "72%" }, ], tooltip: { axisPointer: { type: "cross" }, trigger: "axis", }, xAxis: [ buildCategoryAxis(dates, 0, false), buildCategoryAxis(dates, 1, false), buildCategoryAxis(dates, 2, true), ], yAxis: [ buildValueAxis(0), buildValueAxis(1), buildValueAxis(2, { max: 120, min: -20 }), ], } } function toCandlestickValue(point: SelectionChartPoint) { if ( point.open === null || point.close === null || point.low === null || point.high === null ) { return [Number.NaN, Number.NaN, Number.NaN, Number.NaN] } return [point.open, point.close, point.low, point.high] } function buildCategoryAxis( dates: string[], gridIndex: number, showLabels: boolean, ) { return { axisLabel: { show: showLabels }, axisLine: { onZero: false }, boundaryGap: false, data: dates, gridIndex, min: "dataMin", max: "dataMax", scale: true, splitLine: { show: false }, type: "category" as const, } } function buildValueAxis( gridIndex: number, bounds: { max?: number; min?: number } = {}, ) { return { ...bounds, axisLabel: { fontSize: 10 }, gridIndex, scale: true, splitLine: { lineStyle: { opacity: 0.16 } }, type: "value" as const, } }