feat: K线图默认展示近3个月,拖动回看更早数据;自定义日期格式;主图加高成交量压低

This commit is contained in:
Sakurasan
2026-09-02 01:13:26 +08:00
parent 9a72f0bbf6
commit 375d196fef
3 changed files with 95 additions and 17 deletions
+73 -5
View File
@@ -12,6 +12,7 @@ import {
ColorType,
CrosshairMode,
LineStyle,
TickMarkType,
createSeriesMarkers,
type IChartApi,
type ISeriesApi,
@@ -41,6 +42,8 @@ interface Props {
mode: "line" | "candle";
hasAddedDate?: boolean;
indicators: IndicatorToggles;
/** 默认只展示最近 N 个自然日(日线用),其余数据靠拖动查看;不传则铺满全部数据 */
defaultVisibleDays?: number;
}
// lightweight-charts 无法解析 CSS 变量或 oklch() 颜色,直接用具体 hex 色。
@@ -61,9 +64,41 @@ const MACD_DIF = "#3b82f6";
const MACD_DEA = "#f59e0b";
const RSI_COLOR = "#a855f7";
export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
// 时间统一按 UTC 取值格式化(kline-card 把北京时间墙钟视作 UTC 存储),
// 用本地时区方法会导致日期错位一天
function timeToDate(t: Time): Date {
if (typeof t === "number") return new Date(t * 1000);
if (typeof t === "string") return new Date(t.length === 10 ? `${t}T00:00:00Z` : t);
return new Date(Date.UTC(t.year, t.month - 1, t.day));
}
const pad2 = (n: number) => String(n).padStart(2, "0");
/** 十字光标处的完整日期:2026-09-01 */
function formatCrosshairTime(t: Time): string {
const d = timeToDate(t);
return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
}
/** 底部时间轴刻度:年→2026、月→2026-09、日→09-01(替代默认的 "01 9月 '26") */
function formatTickMark(t: Time, tickMarkType: TickMarkType): string {
const d = timeToDate(t);
if (tickMarkType === TickMarkType.Year) return `${d.getUTCFullYear()}`;
if (tickMarkType === TickMarkType.Month) return `${d.getUTCFullYear()}-${pad2(d.getUTCMonth() + 1)}`;
return `${pad2(d.getUTCMonth() + 1)}-${pad2(d.getUTCDate())}`;
}
// 各 pane 固定高度:主图加高、成交量压低,指标副图居中
const MAIN_PANE_H = 320;
const VOL_PANE_H = 70;
const IND_PANE_H = 95;
export function KLineChart({ data, mode, hasAddedDate, indicators, defaultVisibleDays }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const chartRef = useRef<IChartApi | null>(null);
// 记录上次填充的数据集:仅数据集变化(首次加载/切换周期)时调整视口,
// 指标开关/显示方式切换时保留用户拖动缩放后的位置
const lastDataRef = useRef<KLineItem[] | null>(null);
const priceSeriesRef = useRef<ISeriesApi<"Candlestick" | "Line"> | null>(null);
const volSeriesRef = useRef<ISeriesApi<"Histogram"> | null>(null);
// 指标 series(重建用)
@@ -73,8 +108,15 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
// 容器高度随指标 pane 数量增长,避免主图被压缩
const extraPanes = [indicators.macd, indicators.rsi].filter(Boolean).length;
// 固定图表高度:主图+成交量 300,每个指标副图 +95
const chartHeight = 300 + extraPanes * 95;
// 图表总高 = 各 pane 高度之和(主图 + 成交量 + 指标副图×N)
const chartHeight = MAIN_PANE_H + VOL_PANE_H + extraPanes * IND_PANE_H;
// pane 顺序固定:0=主图 1=成交量 2+=指标副图;指标开关增删 pane 后重新应用高度
function applyPaneHeights(chart: IChartApi) {
chart.panes().forEach((pane, i) => {
pane.setHeight(i === 0 ? MAIN_PANE_H : i === 1 ? VOL_PANE_H : IND_PANE_H);
});
}
// 创建图表(仅一次):pane0 主图 + pane1 成交量
useEffect(() => {
@@ -94,7 +136,12 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
horzLines: { color: GRID, style: LineStyle.Dashed, visible: true },
},
rightPriceScale: { borderColor: GRID },
timeScale: { borderColor: GRID, timeVisible: false },
localization: { timeFormatter: formatCrosshairTime },
timeScale: {
borderColor: GRID,
timeVisible: false,
tickMarkFormatter: formatTickMark,
},
crosshair: { mode: CrosshairMode.Normal },
});
chartRef.current = chart;
@@ -109,6 +156,7 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
1,
);
volSeriesRef.current = vol;
applyPaneHeights(chart);
return () => {
chart.remove();
@@ -118,6 +166,8 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
maSeriesRef.current = [];
macdSeriesRef.current = [];
rsiSeriesRef.current = [];
// 图表实例销毁后重置,重建时重新应用默认视口(StrictMode 重挂载同样生效)
lastDataRef.current = null;
};
}, []);
@@ -223,6 +273,8 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
);
rsiSeriesRef.current.push(s);
}
applyPaneHeights(chart);
}
// 模式切换:重建价格 series + 指标
@@ -310,7 +362,23 @@ export function KLineChart({ data, mode, hasAddedDate, indicators }: Props) {
createSeriesMarkers(ps, []);
}
chart.timeScale().fitContent();
if (lastDataRef.current === data) return;
lastDataRef.current = data;
if (defaultVisibleDays) {
// 默认视口只展示最近 N 个自然日,更早的数据靠向左拖动查看
const toMs = (t: string | number) =>
typeof t === "number" ? t * 1000 : new Date(t).getTime();
const lastMs = toMs(data[data.length - 1].time);
let fromIdx = data.findIndex(
(d) => toMs(d.time) >= lastMs - defaultVisibleDays * 86400_000,
);
if (fromIdx < 0) fromIdx = 0;
chart
.timeScale()
.setVisibleLogicalRange({ from: fromIdx, to: data.length - 1 + 2 });
} else {
chart.timeScale().fitContent();
}
}
// 颜色图例:根据启用的指标生成