Skip to content

架构

Chart Controller → Pane → Renderer

Chart

管理图表的整个生命周期和渲染流程。

初始化 Chart 实例后,会执行 initPanes 方法,在 canvas-layer 下动态添加 Canvas DOM,为每个 Pane 渲染器提供独立的 plotCanvas(图表区域)和 yAxisCanvas (价格纵轴)DOM。

重绘机制

重绘采用 RAF 节流,取消未绘制帧,确保每次只绘制最新请求帧。(给一个 null,跟随对象的成员变量,如果是 null,取消上个绘制帧,然后再申请新帧)

进入 draw 方法后:

  1. 计算 plot 视口信息
  2. 重置 x 轴的变换、缩放,然后清空 x 轴区域
  3. 计算 K 线数据渲染区间
  4. 遍历所有 Pane 渲染器,全部重绘(重构重点)
  5. 绘制 x 轴
  6. 绘制十字线(垂直线在所有 pane 上绘制,水平线只在活跃的 pane 上绘制)
  7. 绘制 MA 图例(在主图上显示均线值:MA 5: 123.45 MA 10: 124.56)
  8. 绘制全局边框(外边框和 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 线的索引转换为逻辑像素位置:

world_X=kGap+index×(kWidth+kGap)
渲染坐标: screenX=worldXscrollLeft
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

可视区最高/最低价标注。

注册缩放回调