# 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线的拖拽滑动展示。 ## 效果预览 ![效果预览](./Screenshots/StockChartPreview.gif) ## 约束与限制 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/)