架构
Chart Controller → Pane → Renderer
Chart
管理图表的整个生命周期和渲染流程。
初始化 Chart 实例后,会执行 initPanes 方法,在 canvas-layer 下动态添加 Canvas DOM,为每个 Pane 渲染器提供独立的 plotCanvas(图表区域)和 yAxisCanvas (价格纵轴)DOM。
重绘机制
重绘采用 RAF 节流,取消未绘制帧,确保每次只绘制最新请求帧。(给一个 null,跟随对象的成员变量,如果是 null,取消上个绘制帧,然后再申请新帧)
进入 draw 方法后:
- 计算 plot 视口信息
- 重置 x 轴的变换、缩放,然后清空 x 轴区域
- 计算 K 线数据渲染区间
- 遍历所有 Pane 渲染器,全部重绘(重构重点)
- 绘制 x 轴
- 绘制十字线(垂直线在所有 pane 上绘制,水平线只在活跃的 pane 上绘制)
- 绘制 MA 图例(在主图上显示均线值:MA 5: 123.45 MA 10: 124.56)
- 绘制全局边框(外边框和 pane 之间的分隔线)
Pane
Pane:代表一个“窗口区域”(主图 / 副图)。
- 记录自身布局(top/height)
- 维护可视价格范围(priceRange)
- 拥有独立的 Y 轴缩放器(PriceScale)
- 保存一组渲染器(renderers),在
chart.draw中按顺序执行。
ts
export class Pane {
readonly id: string
top = 0
height = 0
/** 当前 pane 的可视价格范围(用于右侧轴、以及渲染器内部) */
priceRange: PriceRange = { maxPrice: 100, minPrice: 0 }
/** pane 独立 Y 轴 */
readonly yAxis = new PriceScale()
/** 该 pane 的渲染器列表 */
readonly renderers: PaneRenderer[] = []
/**
* @param id pane 标识符(例如 'main'、'sub'),用于在 Chart/Interaction 中识别 pane。
*/
constructor(id: string) {
this.id = id
}
/**
* 设置 pane 的垂直布局。
*
* @param top 相对 plotCanvas 顶部的偏移(逻辑像素)
* @param height pane 高度(逻辑像素)
*/
setLayout(top: number, height: number) {
this.top = top
this.height = Math.max(1, height)
this.yAxis.setHeight(this.height)
}
/**
* 设置 Y 轴上下 padding(影响 priceToY 映射的上下留白)。
*/
setPadding(top: number, bottom: number) {
this.yAxis.setPadding(top, bottom)
}
/**
* 注册一个 pane 级渲染器。
*/
addRenderer(r: PaneRenderer) {
this.renderers.push(r)
}
/**
* 根据当前可见索引区间更新 priceRange,并同步到 yAxis。
*
* @param data 全量 K 线数据
* @param range 当前视口可见的索引范围(由 getVisibleRange 计算)
*/
updateRange(data: KLineData[], range: VisibleRange) {
this.priceRange = getVisiblePriceRange(data, range.start, range.end)
this.yAxis.setRange(this.priceRange)
}
}Renderer
Pane 级渲染器接口:在单个 pane 的坐标系中绘制内容。
- 调用前 Chart 已经对
ctx做了translate(0, pane.top),因此 y=0 对应 pane 顶部。 - 如需随滚动的 world 坐标,需要 renderer 内部执行
ctx.translate(-scrollLeft, 0)。
ts
export interface PaneRenderer {
draw(args: {
ctx: CanvasRenderingContext2D
pane: Pane
data: KLineData[]
range: VisibleRange
scrollLeft: number
kWidth: number
kGap: number
dpr: number
}): void
}坐标系统
| 坐标系 | 功能 | 原点 | 范围 | 随滚动变化 | 使用场景 |
|---|---|---|---|---|---|
| 世界坐标 | 用于将 K 线的索引转换为逻辑像素位置: 渲染坐标: | K 线数据起始位置 | 整个数据长度 | ❌ 不变 | K 线、网格线、MA 线等数据绘制 |
| 屏幕坐标 | Canvas 绘制坐标系 | 视口左上角 | 可视区域 | ✅ 变化 | 十字线、交互、标签 |
通过世界坐标系,映射 K 线数据索引
初始化流程
创建 Chart 实例
将 canvas 的 DOM 实例传入 Chart 对象实例,以及图表相应的绘制配置信息(K 线宽度、K 线间距、图表距离 Pane 的 padding,右侧价格轴宽度、日期轴高度、缩放下 K 线宽度上下限,Pane 的 id 和 pane 高度占比(PaneSpec))。
ts
const chart = new Chart(
{ container, canvasLayer, xAxisCanvas },
{
kWidth: currentKWidth.value,
kGap: currentKGap.value,
yPaddingPx: props.yPaddingPx,
rightAxisWidth: props.rightAxisWidth,
bottomAxisHeight: props.bottomAxisHeight,
minKWidth: props.minKWidth,
maxKWidth: props.maxKWidth,
panes,
// 主/副图之间真实留白,形成视觉断开
paneGap: 0,
},
);设置渲染器链
Chart 实际上是一个原生实现的状态管理机。渲染器链设置使用其中的 setPaneRenderers 方法。 传入 paneId 来为对应的 pane 应用相应的渲染器组。
ts
/**
* 设置某个 pane 的渲染器链(按顺序执行)。
*
* @param paneId pane 标识(例如 'main'/'sub')
* @param renderers 渲染器数组;会清空并替换原有列表(保持引用稳定)
*/
setPaneRenderers(paneId: string, renderers: Pane['renderers']) {
const renderer = this.paneRenderers.find((r) => r.getPane().id === paneId)
if (!renderer) return
const pane = renderer.getPane()
// 清空并替换(保持引用稳定)
pane.renderers.length = 0
for (const r of renderers) pane.renderers.push(r)
this.scheduleDraw()
}
chart.setPaneRenderers('main', [
GridLinesRenderer,
LastPriceLineRenderer,
CandleRenderer,
ExtremaMarkersRenderer,
createMARenderer(props.showMA),
])
chart.setPaneRenderers('sub', [GridLinesRenderer])渲染器实现
渲染器需实现 PaneRenderer 接口。
GridLinesRenderer
网格线渲染器:仅绘制网格线(不绘制任何文字刻度)。
- 横向:按像素均分(保证永远铺满整个绘图区高度,不受 priceRange 影响)
- 纵向:按月分割(与时间轴刻度保持同一规则,但不画文字)
LastPriceLineRenderer
最新价虚线渲染器。
CandleRenderer
K 线渲染器。
ExtremaMarkersRenderer
可视区最高/最低价标注。