# 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 ``` 浏览器打开 `http://localhost:1789`,拖入或点击 📂 选择 CSV 文件即可。 ## 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` | 覆盖默认配色 | ### 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'` |