在这里插入图片描述
在这里插入图片描述

实例:股票 K 线图(K-Line Chart)|技术:坐标变换(数据域→像素域)、蜡烛图绘制、均线计算、十字光标命中

一、为什么选 K 线图

1.1 金融图表是 Canvas 的"高级科目"

23 数据图表实现了柱状图/折线图/饼图——基础图表。K 线图是进阶:它不只是"把数据画出来",还涉及:

  1. 坐标系变换——价格域(90~110 元)映射到像素域(0~420px),这是所有金融图表的数学地基;
  2. 复合图形——每根 K 线 = 影线(线)+ 实体(矩形),60 根叠加;
  3. 均线叠加——在 K 线之上画 3 条统计折线(MA5/10/20);
  4. 交互命中——触摸定位"最近的一根 K 线",显示 OHLC 数据。

掌握 K 线图 = 掌握"数据可视化 × 交互"的完整链路。

1.2 功能清单

编号 功能 技术要点
1 K 线生成(随机游走模拟) 价格序列生成
2 蜡烛绘制(涨红跌绿) 影线 + 实体
3 网格 + 价格标签 辅助刻度
4 均线 MA5/10/20 滑动窗口平均
5 十字光标 + OHLC 浮层 触摸命中检测
6 换数据 重新生成

二、数据模型

/** K 线数据 */
interface KLine {
  open: number;    // 开盘
  high: number;    // 最高
  low: number;     // 最低
  close: number;   // 收盘
}

OHLC 四价:一根 K 线描述一个时间单位的四个价格。涨跌判定 close >= open——实体颜色由开收比较决定

2.1 模拟数据生成(随机游走)

private generateData(): void {
  const list: KLine[] = [];
  let price = 100;
  for (let i = 0; i < 60; i++) {
    const change = (Math.random() - 0.48) * 4;   // 轻微上涨偏置
    const open = price;
    const close = Math.max(10, open + change);
    const high = Math.max(open, close) + Math.random() * 2;
    const low = Math.min(open, close) - Math.random() * 2;
    list.push({ open: open, high: high, low: low, close: close });
    price = close;   // 下一根的开盘 = 本根收盘
  }
  this.data = list;
}

随机游走(Random Walk)open = 前一根的 close(连续交易的价格衔接),close = open + 随机变动high/low 围绕 [open, close] 上下随机扩展。涨跌偏置 0.48(<0.5)制造轻微上涨趋势——K 线图更有"行情感"。

2.2 价格范围

let max = 0, min = Number.MAX_VALUE;
for (const k of list) {
  max = Math.max(max, k.high);
  min = Math.min(min, k.low);
}
this.maxPrice = max;
this.minPrice = min;

价格域 [minPrice, maxPrice]:遍历所有 K 线取 high 的最大值、low 的最小值——Y 轴的刻度范围。坐标变换的输入。

三、坐标变换 ★核心数学

/** 价格 → 像素 Y(数据域 → 像素域坐标变换) */
private priceToY(price: number): number {
  const range = this.maxPrice - this.minPrice;
  if (range <= 0) {
    return this.padT;   // 防御:无价格波动
  }
  const plotH = this.canvasH - this.padT - this.padB;
  const t = (price - this.minPrice) / range;     // ① 归一化 0~1
  return this.padT + (1 - t) * plotH;            // ② 映射到像素 + 翻转 Y
}

/** K 线索引 → 像素 X 中心 */
private indexToX(idx: number): number {
  return this.padL + idx * (this.candleW + this.gap) + this.candleW / 2;
}

线性映射三步

① 归一化t = (price - min) / (max - min)——把价格压缩到 [0,1]。price=min → t=0;price=max → t=1。归一化是坐标变换的核心,任何数据域都能这样映射到 [0,1]。

② 翻转 + 缩放(1 - t) 因为屏幕 y 向下增长而价格应该"高在上"——t=1(最高价)要映射到 y 最小(顶部),所以翻转。再乘以绘图区高度 plotH

③ 平移:加 padT 偏移到绘图区顶部(预留价格标签区)。plotH = canvasH - padT - padB绘图区净高(扣除上下留白)。

X 轴映射:第 idx 根 K 线的中心 X = 左留白 + idx×(蜡烛宽+间距) + 半宽。索引 → 位置的线性排布。

防御range <= 0(所有价格相同)时返回 padT 避免除以零——坐标变换的边界防护

四、蜡烛图绘制 ★核心绘制

private drawCandles(): void {
  for (let i = 0; i < this.data.length; i++) {
    const k = this.data[i];
    const x = this.indexToX(i);
    const up = k.close >= k.open;   // 涨红 跌绿(A股配色)
    this.ctx.strokeStyle = up ? '#EF4444' : '#10B981';
    this.ctx.fillStyle = up ? '#EF4444' : '#10B981';
    // 影线(最高-最低)
    this.ctx.lineWidth = 1;
    this.ctx.beginPath();
    this.ctx.moveTo(x, this.priceToY(k.high));
    this.ctx.lineTo(x, this.priceToY(k.low));
    this.ctx.stroke();
    // 实体(开-收)
    const yOpen = this.priceToY(k.open);
    const yClose = this.priceToY(k.close);
    const top = Math.min(yOpen, yClose);
    const h = Math.max(1, Math.abs(yOpen - yClose));
    this.ctx.fillRect(x - this.candleW / 2, top, this.candleW, h);
  }
}

一根 K 线的两个部分

部分 绘制 含义
影线 竖线(high → low) 最高价到最低价的波动范围
实体 矩形(open ↔ close) 开盘到收盘的涨跌主体

实体细节

  • top = Math.min(yOpen, yClose):矩形顶部取较小的 y(涨跌都从上方开始画);
  • h = Math.abs(yOpen - yClose):实体高度 = 开收像素差;
  • Math.max(1, h)十字星(开=收)时高度为 0,强制 1px避免矩形不可见;
  • x - candleW/2:矩形以 X 中心线为轴对称。

涨红跌绿:A 股惯例红涨绿跌(与欧美相反),close >= open ? 红 : 绿。颜色语义通过三元选择——一行代码承载配色规则

五、均线绘制

private drawMA(period: number, color: string): void {
  this.ctx.strokeStyle = color;
  this.ctx.lineWidth = 1.5;
  this.ctx.beginPath();
  let started = false;
  for (let i = period - 1; i < this.data.length; i++) {
    // 计算 period 日均线
    let sum = 0;
    for (let j = i - period + 1; j <= i; j++) {
      sum += this.data[j].close;
    }
    const ma = sum / period;
    const x = this.indexToX(i);
    const y = this.priceToY(ma);
    if (!started) {
      this.ctx.moveTo(x, y);
      started = true;
    } else {
      this.ctx.lineTo(x, y);
    }
  }
  this.ctx.stroke();
}

均线(Moving Average):第 i 天的 MA5 = 最近 5 天收盘价的平均。滑动窗口for j = i-4 to i 求和取平均。

起始点iperiod - 1 开始(前 period-1 天数据不足无法计算)——均线起点在 K 线序列的右侧缩进。moveTo 首点 + lineTo 后续:折线绘制的标准手法(started 标志区分首点)。

三条均线:MA5 蓝、MA10 橙、MA20 紫——drawMAs() 依次调用,颜色与图例一致。参数化方法 drawMA(period, color) 一次实现三线复用。

六、网格与价格标签

private drawGrid(): void {
  const plotH = this.canvasH - this.padT - this.padB;
  this.ctx.font = '10px sans-serif';
  this.ctx.fillStyle = '#94A3B8';
  for (let i = 0; i <= 4; i++) {
    const y = this.padT + (plotH / 4) * i;
    // 水平网格线
    this.ctx.strokeStyle = '#F1F5F9';
    this.ctx.lineWidth = 1;
    this.ctx.beginPath();
    this.ctx.moveTo(this.padL, y);
    this.ctx.lineTo(this.canvasW - this.padR, y);
    this.ctx.stroke();
    // 价格标签(左边缘)
    const price = this.maxPrice - (this.maxPrice - this.minPrice) * (i / 4);
    this.ctx.fillText(price.toFixed(2), 2, y - 3);
  }
}

5 等分水平网格:绘图区高度 4 等分,画 5 条水平线(i=0~4)。价格标签:每条网格线对应的价格 = max - range × (i/4)(从高到低)——与 priceToY 的逆映射。fillText 左侧 2px:标签在绘图区左外(padL 预留 44px 放标签)。

七、技术要点对照表

技术点 实现方式 生产价值
坐标变换 归一化 + 翻转 + 平移 数据可视化的数学核心
蜡烛绘制 影线 + 实体 金融图表基础
涨红跌绿 三元颜色 配色规则一行承载
均线 滑动窗口平均 趋势分析
折线绘制 moveTo + lineTo 通用折线
价格标签 逆映射 + fillText 刻度可读
随机游走 open=前close + 偏置 模拟行情
边界防御 range<=0 处理 坐标变换安全

八、文章小结

本篇完成了 K 线图的数据与绘制层:OHLC 数据模型 + 随机游走生成坐标变换(归一化→翻转→平移)把价格域映射到像素域蜡烛图(影线+实体)绘制均线滑动窗口计算网格与价格标签。这套"数据域↔像素域"的映射是金融图表乃至一切数据可视化的数学地基——掌握 priceToY/indexToX 两个函数,就掌握了图表绘制的钥匙。

下一篇《十字光标与触摸交互》将实现:触摸命中检测(最近 K 线索引)、十字光标虚线、OHLC 数据浮层、涨跌色条。


九、深度扩展

1. 成交量副图

K 线下方通常有成交量柱。扩展:第二个绘图区(Y 轴按成交量范围),柱高 = 成交量映射。多绘图区布局是专业图表的分区思路:

// 价格区:padT ~ plotH*0.6
// 成交量区:plotH*0.6 ~ canvasH

2. 时间轴标签

X 轴底部显示日期(每 N 根标一个)。需要 KLine 数据带日期字段 + fillText 在底部。时间刻度是金融图表的标配。

3. 缩放与平移

  • 缩放:触摸双指(Pinch)改变 candleW(放大缩小蜡烛宽度);
  • 平移:拖动改变起始索引(视口平移)。
    手势驱动的视口控制是交互式图表的进阶(数据量大时只画可见区间)。

4. 分时图

K 线是"时间聚合"(日/时),分时图是"逐笔连线"(当日价格曲线 + 均价线)。数据模型不同(时间序列点),绘制类似折线。K 线与分时是两种视角

5. FAQ

Q1:为什么价格标签放左侧而 padL 是 44?
A:左侧要容纳"最高价标签"宽度(如 “112.35” 约 5 字符 × 6px + 边距)。44px 是经验值,标签不遮蜡烛。留白大小 = 标签内容宽度 + 边距

Q2:涨红跌绿为什么用 close >= open 而非 >?
A:>= 把"平盘"(开=收)归为涨(红)——A 股惯例十字星算红或绿无定论,选红更醒目。规则要显式定义(注释说明)。

Q3:随机游走的 0.48 偏置意味着什么?
A:(Math.random() - 0.48) 的期望是 0.02(正),每次变动平均涨 0.02 元——60 根后价格整体上行,K 线图呈现牛市形态。偏置参数控制趋势方向(0.5 = 无趋势震荡)。

Q4:均线为什么从 period-1 开始?
A:第 0~period-2 根的数据不足 period 天,无法计算均线。MA20 前 19 根没有值——均线起点缩进是数学必然,非代码缺陷。

Q5:能用真实行情数据吗?
A:能——把 generateData 换成网络请求(行情 API)解析 JSON 填充 data 数组,绘制层零改动。数据源与绘制解耦是图表架构的红利。


十、下篇预告

下一篇《十字光标与触摸交互》将实现:触摸命中检测(坐标→索引反算)、十字光标虚线绘制、OHLC 数据浮层(深色面板 + 涨跌色条)、"换数据"交互。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐