Files
KlineChart_dev/README.md
T

331 lines
11 KiB
Markdown
Raw Normal View History

2026-08-13 20:59:05 +08:00
# KLineChart CSV 可视化 Demo — 项目说明书
## 1. 项目概览
基于 KLineChart v10 的独立 K 线图表可视化 Demo,支持从本地 CSV 文件加载 OHLCV 历史数据,提供周期切换、指标叠加、深浅主题、缩放拖拽、截图导出等完整交互能力。
**技术栈:**
| 技术 | 版本 | 用途 |
|------|------|------|
| KLineChart | 10.0.2 | 核心 K 线渲染引擎 |
| Vite | 5.x | 本地开发服务器 |
| Canvas 2D | 原生 | 图表绘制 |
## 2. 文件结构
```
debug/
├── index.html # 页面结构:工具栏 + 图表容器
├── main.js # 全部业务逻辑(约 600 行)
├── style.css # 界面样式,含深色主题
├── package.json # 依赖声明(klinecharts + vite)
├── package-lock.json # 版本锁定
└── data/
└── eurgbp_m1.csv # 示例数据(EUR/GBP 1分钟,2021-2026,约 186 万行)
```
## 3. 功能清单
| 功能 | 操作方式 | 说明 |
|------|----------|------|
| **CSV 加载** | 📂 按钮选择 / 拖拽 | 自动识别 5 种 CSV 格式 |
| **周期切换** | 1m / 5m / 15m / 1h / 1d | 实时聚合原始 1m 数据 |
| **缩放** | +/−/⟲ 按钮、鼠标滚轮、键盘 +/− | barSpace 范围 2–50 |
| **翻页** | ←/→ 键盘、拖拽、滚动条 | 分页加载(每次 500 根) |
| **指标** | 工具栏按钮 或 右键菜单 | MA / BOLL / MACD / RSI / VOL |
| **深色主题** | ☾/☀ 按钮 或 D 键 | `chart.setStyles('dark')` |
| **截图** | ▣ 按钮 或 S 键 | `getConvertPictureUrl()` 下载 PNG |
| **十字光标面板** | 鼠标移动 | 顶部实时显示 OHLCV + 涨跌幅 |
| **柱体详情** | 左键点击柱体 | 弹出详细 OHLCV 浮窗 |
| **右键菜单** | 右键图表 | 切换指标 / 主题 / 截图 |
| **自定义指标** | `registerIndicator()` | 扩展任意指标 |
## 4. CSV 数据格式
自动识别以下格式(列顺序可自定义,支持带表头/无表头):
### 格式 A:Binance 格式(时间戳 + OHLCV)
```
1630000000000,0.00145000,0.00145000,0.00144000,0.00144000,5259702
```
### 格式 B:带表头格式
```
timestamp,open,high,low,close,volume
1630000000000,0.00145000,0.00145000,0.00144000,0.00144000,5259702
```
### 格式 C:点号日期格式(当前示例)
```
2021.07.06,00:10,0.85627,0.85629,0.85625,0.85628,7000000.0
```
列顺序:`日期, 时间, O, H, L, C, V`
### 格式 D:横杠日期格式
```
2021-07-06,00:10,0.85627,0.85629,0.85625,0.85628,7000000.0
```
### 格式 E:YYYYMMDD 格式
```
20210706,0.85627,0.85629,0.85625,0.85628,7000000.0
```
**时间戳自动处理:** 13 位数字 → 毫秒;10 位数字 → 秒(自动 ×1000)
## 5. 快速开始
```bash
cd debug
npm install
npm run dev
```
2026-08-13 21:03:50 +08:00
浏览器打开 `http://localhost:1789`,拖入或点击 📂 选择 CSV 文件即可。
2026-08-13 20:59:05 +08:00
## 6. 核心代码结构(main.js)
```
┌─────────────────────────────────────┐
│ loadRawCSV(source) │ 1. 读取文件(File 或 URL)
│ └─ parseCSV(text) │ 2. 自动识别格式,解析到 TypedArray
├─────────────────────────────────────┤
│ aggregateBars(data, range, span) │ 3. 1m → 5m/15m/1h/1d 聚合
├─────────────────────────────────────┤
│ loadDataLoader() │ 4. 返回 setDataLoader 参数
│ └─ getBars({type, timestamp…}) │ 5. init/forward/backward 分页
├─────────────────────────────────────┤
│ mountChart(source) │ 6. init → setSymbol → setPeriod
│ ├─ chart.setDataLoader(...) │ 7. setDataLoader → 图表加载数据
│ ├─ chart.createIndicator(...) │ 8. 创建指标
│ └─ subscribeAction(...) │ 9. 绑定 crosshair/click 事件
└─────────────────────────────────────┘
```
## 7. 自定义指标开发
### 7.1 注册指标
在 `init()` 调用之前注册:
```js
import { registerIndicator, init } from 'klinecharts'
registerIndicator({
name: 'MYAVG',
shortName: 'MYAVG',
series: 'price',
calcParams: [5, 10, 20],
precision: 5,
shouldOhlc: true,
figures: [
{ key: 'a1', title: 'A5: ', type: 'line' },
{ key: 'a2', title: 'A10: ', type: 'line' },
{ key: 'a3', title: 'A20: ', type: 'line' }
],
regenerateFigures: (params) =>
params.map((p, i) => ({ key: `a${i+1}`, title: `A${p}: `, type: 'line' })),
calc: (dataList, indicator) => {
const sums = indicator.calcParams.map(() => 0)
return dataList.map((bar, i) => {
const out = {}
indicator.calcParams.forEach((p, idx) => {
sums[idx] += bar.close
if (i >= p - 1) out[`a${idx+1}`] = sums[idx] / (i + 1)
})
return out
})
}
})
// 注册后再 init
const chart = init('chart', { /* options */ })
chart.createIndicator({ name: 'MYAVG', paneId: 'new' })
```
### 7.2 字段说明
| 字段 | 必需 | 类型 | 说明 |
|------|------|------|------|
| `name` | 是 | string | 唯一标识,用于 `createIndicator({ name })` 匹配 |
| `shortName` | 是 | string | 工具提示中显示的简称 |
| `series` | 是 | `'price' \| 'volume' \| 'normal'` | 系列类型,决定 Y 轴行为 |
| `calcParams` | 否 | `number[]` | 计算参数,可被 `overrideIndicator` 修改 |
| `precision` | 否 | number | 结果数值的小数位数 |
| `shouldOhlc` | 否 | boolean | 计算是否需要 OHLC 字段 |
| `figures` | 是 | `Figure[]` | 图形配置,定义画几条线/柱 |
| `calc` | 是 | `() => D[]` | 核心计算函数 |
| `minValue` / `maxValue` | 否 | number | 固定 Y 轴最小/最大值 |
| `styles` | 否 | `Partial<IndicatorStyle>` | 覆盖默认配色 |
### 7.3 Figure 类型
| `type` | 说明 |
|--------|------|
| `line` | 折线 |
| `bar` | 柱状图(支持 `baseValue: 0` 画零轴柱) |
| `circle` | 圆点 |
| `text` | 文字标注 |
每个 `figure` 至少需要 `key` 和 `type`,`key` 对应 `calc` 返回数据中的字段名。
### 7.4 计算函数约定
```js
calc: (dataList: KLineData[], indicator) => D[]
```
- `dataList` 是当前可见窗口内的 K 线数据(每根含 `timestamp/open/high/low/close/volume`)
- 返回数组长度等于 `dataList` 长度
- 每项为对象,键名与 `figures[].key` 对应
- 数据不足时某键可省略(不会画该点),例如 EMA 前 N 个点
## 8. KLineChart v10 API 速查
### 8.1 图表生命周期
| 方法 | 说明 |
|------|------|
| `init(container, options?)` | 创建图表实例 |
| `dispose(container)` | 销毁,释放资源 |
| `setSymbol({ ticker, pricePrecision, volumePrecision })` | 设置交易品种信息 |
| `setPeriod({ span, type })` | 设置周期,会触发数据重载 |
| `setDataLoader(loader)` | 设置数据加载器 |
| `resetData()` | 强制重新加载数据 |
**调用顺序:** `init` → `setSymbol` → `setPeriod` → `setDataLoader`
### 8.2 指标
| 方法 | 说明 |
|------|------|
| `createIndicator(name, isStack?)` | 添加指标,`isStack=true` 叠加主图 |
| `createIndicator({ name, paneId: 'new' })` | 新建独立面板 |
| `createIndicator({ name, paneId: 'candle_pane' }, true)` | 叠加主图 |
| `removeIndicator({ name })` | 移除指标 |
| `overrideIndicator({ id, calcParams: [...] })` | 修改指标参数 |
| `registerIndicator(template)` | 注册自定义指标(需 `init` 前调用) |
| `getSupportedIndicators()` | 获取内置指标名列表 |
### 8.3 样式与布局
| 方法 | 说明 |
|------|------|
| `setStyles('dark' \| 'light' \| partial)` | 切换主题或局部修改样式 |
| `setBarSpace(value)` | 设置柱间距(等同于缩放) |
| `getBarSpace()` | 获取当前 barSpace `{bar, gapBar}` |
| `setPaneOptions({ paneId, height })` | 设置面板高度 |
| `scrollToRealTime()` | 滚动到最新数据 |
| `scrollByDistance(dist)` | 按像素距离滚动 |
### 8.4 事件订阅
| Action 类型 | 触发时机 | 回调参数 |
|-------------|----------|----------|
| `onCrosshairChange` | 鼠标移动 | `{ x, y, paneId, kLineData, timestamp, dataIndex }` |
| `onCandleBarClick` | 点击柱体 | 同上 |
| `onZoom` | 滚轮缩放 | `barSpace` |
| `onScroll` | 拖动翻页 | — |
| `onVisibleRangeChange` | 可见范围变化 | 起始/结束索引 |
| `onPaneDrag` | 面板高度拖拽 | `paneId, height` |
### 8.5 截图与导出
```js
const url = chart.getConvertPictureUrl(
false, // 是否包含叠加层
'png', // 'png' | 'jpeg' | 'bmp'
'#ffffff' // 背景色
)
```
## 9. 内置指标一览
| 指标 | 叠加/独立 | 说明 |
|------|----------|------|
| MA | 叠加 | 简单移动平均,参数默认 6/12/20 |
| EMA | 独立 | 指数移动平均 |
| BOLL | 叠加 | 布林带 |
| MACD | 独立 | 异同移动平均 |
| RSI | 独立 | 相对强弱指数 |
| SAR | 独立 | 抛物线转向 |
| BBI | 叠加 | 多空指标 |
| SMA | 叠加 | 简易平均线 |
| VOL | 独立 | 成交量 |
## 10. 扩展场景
### 10.1 接入后端 API
替换 `loadRawCSV()` 中的 `fetch('/data/eurgbp_m1.csv')` 为后端接口:
```js
function loadRawCSV(source) {
return fetch('/api/kline?symbol=BTCUSDT&limit=1000')
.then(r => r.json())
.then(rows => {
// rows 格式: [{t: 1630000000000, o: ..., h: ..., l: ..., c: ..., v: ...}]
// 转换为内部格式 {ts, o, h, l, c, v, len}
const count = rows.length
return {
ts: new Float64Array(rows.map(r => r.t)),
o: new Float64Array(rows.map(r => r.o)),
h: new Float64Array(rows.map(r => r.h)),
l: new Float64Array(rows.map(r => r.l)),
c: new Float64Array(rows.map(r => r.c)),
v: new Float64Array(rows.map(r => r.v || 0)),
len: count
}
})
}
```
`getBars` 中的 `forward/backward` 分页自动交给后端处理。
### 10.2 WebSocket 实时推送
```js
chart.setDataLoader({
getBars: ({ type, timestamp, period, callback }) => { /* REST 历史 */ },
subscribeBar: ({ symbol, period, callback }) => {
ws = new WebSocket(`wss://.../ws?symbol=${symbol}`)
ws.onmessage = (evt) => {
const bar = JSON.parse(evt.data)
// 回调单根 K 线,相同 timestamp 更新最后根
callback(bar)
}
},
unsubscribeBar: () => { ws.close() }
})
```
### 10.3 多图表联动
```js
const chart1 = init('chart1')
const chart2 = init('chart2')
chart1.subscribeAction('onCrosshairChange', (crosshair) => {
// 通过 executeAction 同步到另一图表
chart2.executeAction('onCrosshairChange', { x: crosshair.x, y: null })
})
```
## 11. 故障排查
| 现象 | 可能原因 | 解决方法 |
|------|----------|----------|
| 图表空白 | 调用顺序不对 | 确认 `init → setSymbol → setPeriod → setDataLoader` |
| 无数据 | getBars 未调用 callback | 确保 callback 一定被调用(即使失败) |
| 时间错乱 | 时间戳单位错误 | 确认 `timestamp` 为毫秒 |
| 分页失败 | more 标记错误 | 检查 `{ forward, backward }` 返回值 |
| 指标不显示 | name 不匹配 | `createIndicator` 的 name 必须等于注册时的 name |
| 滚动条消失 | data 太少 | 数据量 < 2 不会显示滚动条 |
| 深色/浅色切换失败 | 样式名拼写错误 | 使用 `'dark'` / `'light'` |