# StockChartX
**Repository Path**: yaozhuang1/stock-chart-x
## Basic Information
- **Project Name**: StockChartX
- **Description**: 【鸿蒙 Harmony Next 示例 代码】本示例基于canvas实现了股票行情类分时线与日K线绘制,实现了分时线与日K线的动态刷新,日K线的放大与缩小,日K线的拖拽滑动展示。
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2025-05-20
- **Last Updated**: 2025-05-20
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 基于Canvas实现股票行情类分时线/日K线
## 介绍
本示例基于canvas实现了股票行情类分时线与日K线绘制,实现了分时线与日K线的动态刷新,日K线的放大与缩小,日K线的拖拽滑动展示。
## 效果预览

## 约束与限制
1. 本示例仅支持标准系统上运行,支持设备:华为手机。
2. DevEco Studio版本:DevEco Studio 5.0.3 Release及以上。
3. HarmonyOS SDK版本:HarmonyOS 5.0.3 Release SDK及以上。
## 使用说明
本示例使用模拟数据进行绘图,真实请用网络请求读取。
## 权限说明
无
## 实现思路
### 1.分时线实现
#### 分时图数据格式:
```typescript
interface TimeLineData {
time: string; // 格式 "HH:MM"
price: number;
}
```
#### 绘制思路:
##### (1) 初始化数据,计算当前价格范围
```typescript
private loadInitialData(time: string) {
const index = this.findCurrentTimeIndex(time);
if (index >= 0) {
this.currentIndex = index;
this.currentData = this.allData.slice(0, index + 1); // 只加载当前时间点数据
this.calculatePriceRange();
}
}
```
##### (2) 确认数据在X轴与Y轴的坐标
```typescript
private timeToX(time: string): number {
const totalMinutes = this.getTotalMinutes(time);
const startMinutes = this.getTotalMinutes('09:30');
const endMinutes = this.getTotalMinutes('15:00');
const lunchStart = this.getTotalMinutes('11:30');
const lunchEnd = this.getTotalMinutes('13:00');
const availableWidth = this.getCanvasWidth() - this.timeLinePadding.left - this.timeLinePadding.right;
// 计算总交易时间(减去休市时间)
const totalTradingMinutes = (endMinutes - startMinutes) - (lunchEnd - lunchStart);
// 调整时间计算,跳过休市时间
let adjustedMinutes = 0;
if (totalMinutes <= lunchStart) {
// 上午交易时间
adjustedMinutes = totalMinutes - startMinutes;
} else {
// 下午交易时间(减去休市时间)
adjustedMinutes = (totalMinutes - startMinutes) - (lunchEnd - lunchStart);
}
return this.timeLinePadding.left + availableWidth * adjustedMinutes / totalTradingMinutes;
}
private priceToY(price: number): number {
const availableHeight = this.getCanvasHeight() - this.timeLinePadding.top - this.timeLinePadding.bottom;
return this.timeLinePadding.top + availableHeight * (1 - (price - this.minPrice) / (this.maxPrice - this.minPrice));
}
```
##### (3) 绘制背景和网格
##### (4) 绘制分时线
```typescript
private drawTimeLine(ctx: CanvasRenderingContext2D) {
if (this.currentData.length === 0) {
return;
}
ctx.beginPath();
this.currentData.forEach((point, i) => {
const x = this.timeToX(point.time);
const y = this.priceToY(point.price);
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
});
ctx.strokeStyle = '#4B87BE';
ctx.lineWidth = 1;
ctx.stroke();
}
```
##### (5) 绘制坐标标签
```typescript
private drawAxisLabels(ctx: CanvasRenderingContext2D) {
const height = this.getCanvasHeight();
// 绘制价格标签
ctx.textAlign = 'right';
ctx.fillStyle = '#666666';
// 最高价标签
ctx.fillText(
this.maxPrice.toFixed(2),
this.timeLinePadding.left - 5,
this.timeLinePadding.top + 35
);
// 最低价标签
ctx.fillText(
this.minPrice.toFixed(2),
this.timeLinePadding.left - 5,
height - this.timeLinePadding.bottom - 30
);
// 当前价格标签(最后一个点)
if (this.currentData.length > 0) {
const lastPoint = this.currentData[this.currentData.length - 1];
const y = this.priceToY(lastPoint.price);
ctx.fillStyle = '#F5222D';
ctx.fillText(
lastPoint.price.toFixed(2),
this.getCanvasWidth() - this.timeLinePadding.right,
y - 5
);
// 绘制当前价格点
ctx.beginPath();
ctx.arc(this.timeToX(lastPoint.time), y, 3, 0, Math.PI * 2);
ctx.fillStyle = '#F5222D';
ctx.fill();
}
```
### 2.日K线实现
#### K线图数据格式
```typescript
interface KLineData {
timestamp: string; // 格式 "YYYYMMDD"
open: number;
close: number;
high: number;
low: number;
}
```
#### 绘制思路:
###### (1) 初始化数据
```typescript
private initializePartialData() {
// 实际情况可以获得最新日期的数据
const targetDate = '20250328';
const fullData = dailyKLineData;
const targetIndex = fullData.findIndex(item => item.timestamp === targetDate);
if (targetIndex !== -1) {
this.allData = fullData.slice(0, targetIndex + 1);
} else {
// 如果找不到目标日期,则加载全部数据
this.allData = [...fullData];
}
}
```
##### (2) 更新展示数据
```typescript
private updateDisplayData() {
// 如果是首次加载或非拖拽状态,则从currentIndex开始展示
if (!this.isDragging) {
this.dragOffset = Math.max(0, this.currentIndex - this.dataNumber + 1);
}
const startIndex = Math.max(0, this.dragOffset);
const endIndex = Math.min(this.allData.length - 1, startIndex + this.dataNumber - 1);
this.currentData = this.allData.slice(startIndex, endIndex + 1);
// 检查当前是否展示最新数据
this.isViewingLatestData = (endIndex === (this.allData.length - 1));
this.calculatePriceRange();
this.candleWidth =
(this.getCanvasWidth() - this.dailyKLinePadding.left - this.dailyKLinePadding.right) / this.dataNumber;
this.redrawCanvas();
}
```
##### (3) 计算当前展示数据的价格范围
```typescript
private calculatePriceRange() {
if (this.currentData.length === 0) {
return;
}
let min = Infinity, max = -Infinity;
this.currentData.forEach(item => {
min = Math.min(min, item.low);
max = Math.max(max, item.high);
});
const range = max - min;
this.minPrice = min - range * 0.1;
this.maxPrice = max + range * 0.1;
}
```
##### (4) 根据价格计算纵坐标
```typescript
private priceToY(price: number): number {
const canvasHeight = this.getCanvasHeight() - this.dailyKLinePadding.top - this.dailyKLinePadding.bottom;
return this.dailyKLinePadding.top + canvasHeight * (1 - (price - this.minPrice) / (this.maxPrice - this.minPrice));
}
```
##### (5) 蜡烛图绘制
当收盘价>=开盘价时,绘制颜色为红色;否则,绘制颜色为绿色。
```typescript
private drawCandle(ctx: CanvasRenderingContext2D, index: number, candle: KLineData) {
const x = this.dailyKLinePadding.left + index * this.candleWidth;
const width = this.candleWidth * 0.8;
const yHigh = this.priceToY(candle.high);
const yLow = this.priceToY(candle.low);
const yOpen = this.priceToY(candle.open);
const yClose = this.priceToY(candle.close);
const isUp = candle.close >= candle.open;
const color = isUp ? '#F5222D' : '#52C41A';
// 绘制影线
ctx.beginPath();
ctx.moveTo(x + width / 2, yHigh);
ctx.lineTo(x + width / 2, yLow);
ctx.strokeStyle = color;
ctx.stroke();
// 绘制实体
ctx.fillStyle = color;
ctx.fillRect(
x + width * 0.1,
Math.min(yOpen, yClose),
width * 0.8,
Math.abs(yOpen - yClose)
);
}
```
##### (6) 拖拽功能实现
TouchType.Down事件触发时,shouldUpdateView禁止自动更新视图(仍然接收数据,但不绘制最新图象);
event.type === TouchType.Move事件触发时,计算拖拽偏移量;
TouchType.Up事件触发时,检查当前是否移动到最新数据展示。如果是,则启动自动更新;否则展示历史数据。
```typescript
Canvas(this.context)
.onTouch((event: TouchEvent) => {
if (event.type === TouchType.Down) {
this.isDragging = true;
this.shouldUpdateView = false; // 拖拽开始,禁止自动更新视图
this.lastX = event.touches[0].x;
} else if (event.type === TouchType.Move && this.isDragging) {
const deltaX = event.touches[0].x - this.lastX;
this.lastX = event.touches[0].x;
// 计算拖拽偏移量(按比例换算)
const dragDelta = Math.round(deltaX / this.candleWidth);
this.dragOffset = Math.max(0, Math.min(this.maxOffset, this.dragOffset - dragDelta));
// 更新展示数据
this.updateDisplayData();
} else if (event.type === TouchType.Up) {
this.isDragging = false;
// 检查是否拖拽到右侧
if (this.dragOffset >= this.maxOffset) {
this.shouldUpdateView = true; // 恢复自动更新
this.currentIndex = this.allData.length - 1;
this.updateDisplayData();
this.startTimer(); // 恢复自动滚动
}
}
})
```
##### (7) 展示数量控制
dataNumber参数用来控制当前展示的数据数量,范围30~数据最大值
```typescript
Button('-')
.onClick(() => {
this.dataNumber = Math.max(30, this.dataNumber - 10);
this.updateDisplayData();
})
Button('+')
.onClick(() => {
this.dataNumber = Math.min(this.allData.length, this.dataNumber + 10);
this.updateDisplayData();
})
```
## 工程目录
```typescript
├─entry/src/main/ets
│ ├─entryability
│ │ └─EntryAbility.ets // 程序入口,调用云函数
| ├─component
| | ├─DailyKLine.ets // 日K线绘制组件
| | ├─TimeLineChart.ets // 分时线绘制组件
| ├─model
| | ├─Data.ets // 数据定义
│ ├─pages
└─StockChartPage.ets // 主页
```
## 参考文档
[Canvas](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-components-canvas-canvas)
[CanvasRenderingContext2D](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/ts-canvasrenderingcontext2d)
[Canvas绘制内容如何动态更新](https://developer.huawei.com/consumer/cn/doc/harmonyos-faqs/faqs-arkui-225)
### ChangeLog
| 修改日期 | 修改内容 |
|----------|------|
| 20250328 | 初稿 |
## 一份简单的问卷反馈
亲爱的Harmony Next开发者,您好!
为了协助您高效开发,提高鸿蒙场景化示例的质量,希望您在浏览或使用后抽空填写一份简单的问卷,我们将会收集您的宝贵意见进行优化:heart:
[:arrow_right: **点击此处填写问卷** ](https://wj.qq.com/s2/19042938/95ab/)