鸿蒙股票 K 线图:坐标变换与蜡烛图绘制实战


实例:股票 K 线图(K-Line Chart)|技术:坐标变换(数据域→像素域)、蜡烛图绘制、均线计算、十字光标命中
一、为什么选 K 线图
1.1 金融图表是 Canvas 的"高级科目"
23 数据图表实现了柱状图/折线图/饼图——基础图表。K 线图是进阶:它不只是"把数据画出来",还涉及:
- 坐标系变换——价格域(90~110 元)映射到像素域(0~420px),这是所有金融图表的数学地基;
- 复合图形——每根 K 线 = 影线(线)+ 实体(矩形),60 根叠加;
- 均线叠加——在 K 线之上画 3 条统计折线(MA5/10/20);
- 交互命中——触摸定位"最近的一根 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 求和取平均。
起始点:i 从 period - 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 数据浮层(深色面板 + 涨跌色条)、"换数据"交互。
更多推荐




所有评论(0)