This commit is contained in:
2026-08-13 20:59:05 +08:00
commit 6ec17e9d94
7 changed files with 2296 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
node_modules/
data/
*.csv
.DS_Store
+331
View File
@@ -0,0 +1,331 @@
# 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:5173`,拖入或点击 📂 选择 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<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'` |
+49
View File
@@ -0,0 +1,49 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>KLineChart debug</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<div id="root">
<div id="toolbar">
<div id="file-picker" title="选择 CSV 文件,或拖拽文件到此处">
<button id="btn-file" type="button" title="选择 CSV 文件">📂</button>
<span id="file-name">无文件</span>
</div>
<div id="period-switcher" aria-label="周期切换">
<button data-period="1m" type="button">1m</button>
<button data-period="5m" type="button">5m</button>
<button data-period="15m" type="button">15m</button>
<button data-period="1h" type="button">1h</button>
<button data-period="1d" type="button">1d</button>
</div>
<div id="indicator-switcher" aria-label="指标">
<button data-indicator="MA" type="button">MA</button>
<button data-indicator="BOLL" type="button">BOLL</button>
<button data-indicator="MACD" type="button">MACD</button>
<button data-indicator="RSI" type="button">RSI</button>
<button data-indicator="VOL" type="button">VOL</button>
</div>
<div id="zoom-controls">
<button id="zoom-in" type="button" title="放大">+</button>
<button id="zoom-out" type="button" title="缩小">−</button>
<button id="zoom-reset" type="button" title="重置缩放">⟲</button>
</div>
<div id="tool-actions">
<button id="btn-theme" type="button" title="切换主题">☾</button>
<button id="btn-screenshot" type="button" title="截图">▣</button>
</div>
<div id="status"></div>
</div>
<div id="loading" class="loading" style="display:none;">Loading CSV data...</div>
<div id="chart"></div>
<div id="crosshair-info" class="hidden"></div>
<div id="bar-detail" class="popup hidden"></div>
<div id="context-menu" class="popup hidden"></div>
</div>
<script type="module" src="./main.js"></script>
</body>
</html>
+603
View File
@@ -0,0 +1,603 @@
import { dispose, init } from 'klinecharts'
const MINUTE = 60 * 1000
const LOAD_BAR_COUNT = 500
const BAR_SPACE_STEP = 5
const BAR_SPACE_MIN = 2
const BAR_SPACE_MAX = 50
const PERIODS = [
{ key: '1m', period: { span: 1, type: 'minute' }, candleType: 'candle_solid' },
{ key: '5m', period: { span: 5, type: 'minute' }, candleType: 'candle_solid' },
{ key: '15m', period: { span: 15, type: 'minute' }, candleType: 'candle_solid' },
{ key: '1h', period: { span: 1, type: 'hour' }, candleType: 'candle_solid' },
{ key: '1d', period: { span: 1, type: 'day' }, candleType: 'candle_solid' }
]
const INDICATORS = [
{ key: 'MA', name: 'MA', stacked: true },
{ key: 'BOLL', name: 'BOLL', stacked: true },
{ key: 'MACD', name: 'MACD', stacked: false },
{ key: 'RSI', name: 'RSI', stacked: false },
{ key: 'VOL', name: 'VOL', stacked: false }
]
let chart = null
let rawBars = null
let rawLoaded = false
let activePeriodKey = PERIODS[0].key
let isDark = false
let activeIndicators = new Set()
let closePopup = () => {}
let closeCrosshair = () => {}
let fileName = null
function getActivePeriodConfig() {
return PERIODS.find(({ key }) => key === activePeriodKey) ?? PERIODS[0]
}
function parseTimestamp(line, parts, fmt) {
const tsNum = Number(parts[0])
if (!isNaN(tsNum) && tsNum > 1e12 && tsNum < 1e14) return tsNum
if (!isNaN(tsNum) && tsNum > 1e9 && tsNum < 1e12) return tsNum * 1000
if (fmt === 'binance') {
let ts = tsNum
if (ts > 0 && ts < 1e13) ts *= 1000
return ts
}
const dateStr = parts[0]
const timeStr = parts[1] ?? ''
if (dateStr.includes('.')) {
const y = Number(dateStr.slice(0, 4))
const m = Number(dateStr.slice(5, 7)) - 1
const d = Number(dateStr.slice(8, 10))
const h = Number(timeStr.slice(0, 2))
const mi = Number(timeStr.slice(3, 5))
return Date.UTC(y, m, d, h, mi, 0, 0)
}
if (dateStr.includes('-')) {
const y = Number(dateStr.slice(0, 4))
const m = Number(dateStr.slice(5, 7)) - 1
const d = Number(dateStr.slice(8, 10))
const h = Number(timeStr.slice(0, 2) || '0')
const mi = Number(timeStr.slice(3, 5) || '0')
return Date.UTC(y, m, d, h, mi, 0, 0)
}
if (dateStr.length === 8 && /^\d+$/.test(dateStr)) {
const y = Number(dateStr.slice(0, 4))
const m = Number(dateStr.slice(4, 6)) - 1
const d = Number(dateStr.slice(6, 8))
return Date.UTC(y, m, d, 0, 0, 0, 0)
}
if (line.includes(' ')) {
const dt = line.split(/[,\t]/)[0]
const [ds, ts] = dt.split(' ')
if (ds && ts) {
const y = Number(ds.slice(0, 4))
const m = Number(ds.slice(4, 6)) - 1
const d = Number(ds.slice(6, 8))
const h = Number(ts.slice(0, 2))
const mi = Number(ts.slice(3, 5))
return Date.UTC(y, m, d, h, mi, 0, 0)
}
}
return NaN
}
function detectFormat(firstLine) {
const trimmed = firstLine.trim()
const lower = trimmed.toLowerCase()
if (lower.startsWith('timestamp') || lower.startsWith('datetime') || lower.startsWith('time') || lower.startsWith('date')) {
const cols = trimmed.split(/[,\t]/)
const remaining = cols.slice(1).join(',')
return { type: 'header', sub: detectFormat(remaining) ?? 'generic', headerCols: cols }
}
const parts = trimmed.split(/[,\t]/)
if (parts.length >= 6 && /^\d{10,13}$/.test(parts[0])) return 'binance'
if (parts.length >= 6 && parts[0].includes('.')) return 'dot'
if (parts.length >= 5 && parts[0].includes('-')) return 'dash'
if (parts.length >= 4 && parts[0].length === 8 && /^\d+$/.test(parts[0])) return 'yyyymmdd'
return 'generic'
}
function parseCSV(text) {
const lines = text.split(/\r?\n/).filter(l => l.trim().length > 0)
if (lines.length < 1) return null
const fmt = detectFormat(lines[0])
const hasHeader = fmt.type === 'header'
const startIndex = hasHeader ? 1 : 0
if (lines.length <= startIndex) return null
const count = lines.length - startIndex
const tsArr = new Float64Array(count)
const oArr = new Float64Array(count)
const hArr = new Float64Array(count)
const lArr = new Float64Array(count)
const cArr = new Float64Array(count)
const vArr = new Float64Array(count)
let n = 0
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i]
if (!line) continue
const parts = line.split(/[,\t]/)
if (parts.length < 5) continue
let ts, o, h, l, c, v = 0
ts = parseTimestamp(line, parts, fmt)
if (hasHeader) {
o = Number(parts[1])
h = Number(parts[2])
l = Number(parts[3])
c = Number(parts[4])
v = Number(parts[5] || 0)
} else {
o = Number(parts[2])
h = Number(parts[3])
l = Number(parts[4])
c = Number(parts[5])
v = Number(parts[6] || 0)
}
if (Number.isNaN(ts) || !isFinite(o)) continue
tsArr[n] = ts
oArr[n] = o
hArr[n] = h
lArr[n] = l
cArr[n] = c
vArr[n] = v
n++
}
return {
ts: tsArr.slice(0, n),
o: oArr.slice(0, n),
h: hArr.slice(0, n),
l: lArr.slice(0, n),
c: cArr.slice(0, n),
v: vArr.slice(0, n),
len: n
}
}
function loadRawCSV(source) {
const loadText = source instanceof File
? () => source.text()
: () => fetch(source).then(r => {
if (!r.ok) throw new Error('CSV fetch failed: ' + r.status)
return r.text()
})
return loadText().then(text => {
const data = parseCSV(text)
if (!data || data.len === 0) throw new Error('No data parsed from CSV')
return data
})
}
function aggregateBars(data, startIndex, endIndex, spanMinutes) {
if (spanMinutes === 1) {
const result = []
for (let i = startIndex; i < endIndex; i++) {
result.push({
timestamp: data.ts[i],
open: data.o[i],
high: data.h[i],
low: data.l[i],
close: data.c[i],
volume: data.v[i]
})
}
return result
}
const result = []
const spanMs = spanMinutes * MINUTE
let groupStart = startIndex
while (groupStart < endIndex) {
let groupEnd = groupStart + 1
const groupStartTs = Math.floor(data.ts[groupStart] / spanMs) * spanMs
while (groupEnd < endIndex && Math.floor(data.ts[groupEnd] / spanMs) * spanMs === groupStartTs) {
groupEnd++
}
let o = data.o[groupStart]
let h = data.h[groupStart]
let l = data.l[groupStart]
let c = data.c[groupEnd - 1]
let v = 0
for (let i = groupStart; i < groupEnd; i++) {
h = Math.max(h, data.h[i])
l = Math.min(l, data.l[i])
v += data.v[i]
}
result.push({
timestamp: groupStartTs,
open: o,
high: h,
low: l,
close: c,
volume: v
})
groupStart = groupEnd
}
return result
}
function findIndex(data, targetTs) {
let lo = 0
let hi = data.len - 1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (data.ts[mid] < targetTs) lo = mid + 1
else hi = mid - 1
}
return lo
}
function getSpanMinutes(period) {
if (period.type === 'minute') return period.span
if (period.type === 'hour') return period.span * 60
if (period.type === 'day') return period.span * 1440
return 1
}
function loadDataLoader() {
return {
getBars: ({ type, timestamp, period, callback }) => {
if (!rawLoaded) {
callback([], { forward: false, backward: false })
return
}
const span = getSpanMinutes(period)
const duration = span * MINUTE
const bars = []
let forwardMore = false
let backwardMore = false
if (type === 'init') {
const latestTs = rawBars.ts[rawBars.len - 1]
const startTs = latestTs - LOAD_BAR_COUNT * duration
const startIdx = findIndex(rawBars, startTs)
const endIdx = findIndex(rawBars, latestTs + 1)
bars.push(...aggregateBars(rawBars, startIdx, Math.min(endIdx, rawBars.len), span))
forwardMore = startIdx > 0
} else if (type === 'forward') {
const startIdx = findIndex(rawBars, timestamp)
const endIdx = Math.max(0, startIdx - LOAD_BAR_COUNT)
bars.push(...aggregateBars(rawBars, endIdx, startIdx, span))
forwardMore = endIdx > 0
} else if (type === 'backward') {
const startIdx = findIndex(rawBars, timestamp)
const endIdx = Math.min(rawBars.len, startIdx + LOAD_BAR_COUNT)
bars.push(...aggregateBars(rawBars, startIdx, endIdx, span))
forwardMore = false
backwardMore = endIdx < rawBars.len
}
callback(bars, { forward: forwardMore, backward: backwardMore })
updateStatus()
},
subscribeBar: () => {},
unsubscribeBar: () => {}
}
}
// ---- UI helpers ----
function setActivePeriodButton() {
document.querySelectorAll('#period-switcher button').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.period === activePeriodKey)
})
}
function setActiveIndicatorButtons() {
document.querySelectorAll('#indicator-switcher button').forEach((btn) => {
btn.classList.toggle('active', activeIndicators.has(btn.dataset.indicator))
})
}
function updateStatus() {
const el = document.getElementById('status')
if (!el || chart === null) return
const bars = chart.getDataList()
if (bars.length > 0) {
const from = new Date(bars[0].timestamp).toLocaleDateString()
const to = new Date(bars[bars.length - 1].timestamp).toLocaleDateString()
el.textContent = `${from} → ${to} | ${bars.length} bars`
}
}
function formatDate(ts) {
const d = new Date(ts)
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
}
// ---- Chart actions ----
function switchPeriod(periodKey) {
const config = PERIODS.find(({ key }) => key === periodKey)
if (chart === null || config === undefined || activePeriodKey === periodKey) return
activePeriodKey = periodKey
setActivePeriodButton()
chart.setStyles({ candle: { type: config.candleType } })
chart.setPeriod(config.period)
}
function toggleIndicator(key) {
const spec = INDICATORS.find((i) => i.key === key)
if (!spec || chart === null) return
if (activeIndicators.has(key)) {
activeIndicators.delete(key)
chart.removeIndicator({ name: spec.name })
} else {
activeIndicators.add(key)
const opts = spec.stacked
? { name: spec.name, paneId: 'candle_pane' }
: { name: spec.name, paneId: 'new' }
chart.createIndicator(opts, spec.stacked)
}
setActiveIndicatorButtons()
}
function setTheme(dark) {
isDark = dark
document.body.classList.toggle('dark', dark)
const btn = document.getElementById('btn-theme')
btn.textContent = dark ? '☀' : '☾'
btn.classList.toggle('active-icon', dark)
chart.setStyles(dark ? 'dark' : 'light')
}
function doScreenshot() {
if (chart === null) return
const url = chart.getConvertPictureUrl(false, 'png', isDark ? '#0d1117' : '#ffffff')
const a = document.createElement('a')
a.href = url
a.download = `kline-${activePeriodKey}-${Date.now()}.png`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
}
function setBarSpace(value) {
if (chart === null) return
chart.setBarSpace(value)
updateStatus()
}
function doZoomIn() {
if (chart === null) return
const current = chart.getBarSpace().bar
setBarSpace(Math.min(current + BAR_SPACE_STEP, BAR_SPACE_MAX))
}
function doZoomOut() {
if (chart === null) return
const current = chart.getBarSpace().bar
setBarSpace(Math.max(current - BAR_SPACE_STEP, BAR_SPACE_MIN))
}
function doZoomReset() {
if (chart === null) return
setBarSpace(Math.round((BAR_SPACE_MIN + BAR_SPACE_MAX) / 2))
chart.scrollToRealTime()
}
// ---- Crosshair info ----
function formatPrice(v) { return v != null ? v.toFixed(5) : '-' }
function formatVol(v) {
if (v == null) return '-'
if (v >= 1e9) return (v / 1e9).toFixed(2) + 'B'
if (v >= 1e6) return (v / 1e6).toFixed(2) + 'M'
if (v >= 1e3) return (v / 1e3).toFixed(2) + 'K'
return String(v)
}
function onCrosshairHandler(crosshair) {
const el = document.getElementById('crosshair-info')
closeCrosshair()
closeCrosshair = () => { el.classList.add('hidden') }
if (!crosshair || !crosshair.kLineData) {
el.classList.add('hidden')
return
}
el.classList.remove('hidden')
const k = crosshair.kLineData
const d = new Date(k.timestamp)
const timeStr = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')} ${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`
const change = k.close - k.open
const changePct = k.open > 0 ? (change / k.open * 100) : 0
const color = change >= 0 ? (isDark ? '#ff7b72' : '#cf222e') : (isDark ? '#7ee787' : '#1a7f37')
el.innerHTML = `
<div style="color:#8b949e;margin-bottom:2px">${timeStr}</div>
<div>O <b>${formatPrice(k.open)}</b> &nbsp; H <b>${formatPrice(k.high)}</b></div>
<div>L <b>${formatPrice(k.low)}</b> &nbsp; C <b style="color:${color}">${formatPrice(k.close)}</b></div>
<div>Δ <b style="color:${color}">${change >= 0 ? '+' : ''}${change.toFixed(5)} (${changePct >= 0 ? '+' : ''}${changePct.toFixed(3)}%)</b></div>
<div>Vol <b>${formatVol(k.volume)}</b></div>
`
}
// ---- Bar click detail ----
function onBarClickHandler(crosshair) {
const popup = document.getElementById('bar-detail')
closePopup()
if (!crosshair || !crosshair.kLineData || crosshair.x == null) {
popup.classList.add('hidden')
return
}
const k = crosshair.kLineData
const bull = k.close >= k.open
const cls = bull ? 'bull' : 'bear'
popup.classList.remove('hidden')
popup.style.left = Math.min(crosshair.x + 20, window.innerWidth - 200) + 'px'
popup.style.top = Math.min(crosshair.y + 20, window.innerHeight - 200) + 'px'
popup.innerHTML = `
<div style="margin-bottom:6px;font-weight:600">${formatDate(k.timestamp)}</div>
<div class="row"><span class="label">Open</span><span class="value ${cls}">${formatPrice(k.open)}</span></div>
<div class="row"><span class="label">High</span><span class="value">${formatPrice(k.high)}</span></div>
<div class="row"><span class="label">Low</span><span class="value">${formatPrice(k.low)}</span></div>
<div class="row"><span class="label">Close</span><span class="value ${cls}">${formatPrice(k.close)}</span></div>
<div class="row"><span class="label">Volume</span><span class="value">${formatVol(k.volume)}</span></div>
`
closePopup = () => { popup.classList.add('hidden') }
}
// ---- Right-click context menu ----
function showContextMenu(x, y) {
const menu = document.getElementById('context-menu')
const items = INDICATORS.map((ind) => {
const on = activeIndicators.has(ind.key)
return `<div class="item" data-indicator="${ind.key}"><span class="check">${on ? '✓' : ''}</span>${ind.name}</div>`
}).join('')
menu.innerHTML = `
${items}
<div class="sep"></div>
<div class="item" data-action="theme"><span class="check"></span>切换主题 <span class="key-hint">D</span></div>
<div class="item" data-action="screenshot"><span class="check"></span>截图 <span class="key-hint">S</span></div>
`
menu.style.left = Math.min(x, window.innerWidth - 200) + 'px'
menu.style.top = Math.min(y, window.innerHeight - 260) + 'px'
menu.classList.remove('hidden')
closePopup()
closePopup = () => {
menu.classList.add('hidden')
menu.querySelectorAll('.item').forEach((el) => el.removeEventListener('click', handleMenuItem))
}
menu.querySelectorAll('.item').forEach((el) => el.addEventListener('click', handleMenuItem))
}
function handleMenuItem(e) {
const el = e.currentTarget
const ind = el.dataset.indicator
const action = el.dataset.action
if (ind) toggleIndicator(ind)
if (action === 'theme') setTheme(!isDark)
if (action === 'screenshot') doScreenshot()
closePopup()
}
// ---- Keyboard ----
function onKeydown(e) {
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return
if (chart === null) return
switch (e.key) {
case 'ArrowLeft':
chart.scrollByDistance(-3 * chart.getBarSpace().bar)
e.preventDefault()
break
case 'ArrowRight':
chart.scrollByDistance(3 * chart.getBarSpace().bar)
e.preventDefault()
break
case '+':
case '=':
doZoomIn()
e.preventDefault()
break
case '-':
doZoomOut()
e.preventDefault()
break
case 'Escape':
closePopup()
closeCrosshair()
break
case 'd':
setTheme(!isDark)
break
case 's':
if (!e.ctrlKey && !e.metaKey) doScreenshot()
break
}
}
// ---- Toolbar binding ----
function bindToolbar() {
const picker = document.getElementById('file-picker')
const nameEl = document.getElementById('file-name')
const btnFile = document.getElementById('btn-file')
const input = document.createElement('input')
input.type = 'file'
input.accept = '.csv,.txt,.tsv'
input.style.display = 'none'
document.body.appendChild(input)
function setFile(file) {
fileName = file.name.replace(/\.[^.]+$/, '').replace(/[_\s]+/g, ' ').slice(0, 30)
if (nameEl) nameEl.textContent = fileName
mountChart(file)
}
btnFile.addEventListener('click', () => input.click())
input.addEventListener('change', () => {
if (input.files[0]) setFile(input.files[0])
})
picker.addEventListener('dragover', (e) => { e.preventDefault(); picker.classList.add('over') })
picker.addEventListener('dragleave', () => picker.classList.remove('over'))
picker.addEventListener('drop', (e) => {
e.preventDefault()
picker.classList.remove('over')
const file = e.dataTransfer.files[0]
if (file && (file.name.endsWith('.csv') || file.name.endsWith('.txt') || file.name.endsWith('.tsv'))) {
setFile(file)
}
})
document.querySelectorAll('#period-switcher button').forEach((btn) => {
btn.addEventListener('click', () => switchPeriod(btn.dataset.period))
})
document.querySelectorAll('#indicator-switcher button').forEach((btn) => {
btn.addEventListener('click', () => toggleIndicator(btn.dataset.indicator))
})
document.getElementById('zoom-in').addEventListener('click', doZoomIn)
document.getElementById('zoom-out').addEventListener('click', doZoomOut)
document.getElementById('zoom-reset').addEventListener('click', doZoomReset)
document.getElementById('btn-theme').addEventListener('click', () => setTheme(!isDark))
document.getElementById('btn-screenshot').addEventListener('click', doScreenshot)
document.addEventListener('keydown', onKeydown)
document.addEventListener('click', () => { closePopup(); closeCrosshair() })
document.addEventListener('contextmenu', (e) => {
e.preventDefault()
showContextMenu(e.clientX, e.clientY)
})
setActivePeriodButton()
setActiveIndicatorButtons()
}
function mountChart(source) {
const chartDom = document.getElementById('chart')
if (chartDom === null) return
const loadingEl = document.getElementById('loading')
if (loadingEl) loadingEl.style.display = 'block'
loadRawCSV(source).then(data => {
rawBars = data
rawLoaded = true
if (loadingEl) loadingEl.style.display = 'none'
console.log(`Loaded ${data.len} bars, ${new Date(data.ts[0])} → ${new Date(data.ts[data.len - 1])}`)
if (chart) dispose(chartDom)
chart = init(chartDom, {
styles: {
candle: { type: getActivePeriodConfig().candleType },
indicator: { lastValueMark: { show: true } }
},
layout: {
barSpaceLimit: { min: BAR_SPACE_MIN, max: BAR_SPACE_MAX },
basicParams: {}
}
})
chart.setSymbol({ ticker: fileName || 'CSV', pricePrecision: 5, volumePrecision: 0 })
chart.setPeriod(getActivePeriodConfig().period)
chart.setDataLoader(loadDataLoader())
chart.subscribeAction('onCrosshairChange', onCrosshairHandler)
chart.subscribeAction('onCandleBarClick', onBarClickHandler)
setTimeout(updateStatus, 200)
}).catch(err => {
if (loadingEl) loadingEl.textContent = 'Error: ' + err.message
console.error(err)
})
}
window.addEventListener('beforeunload', () => {
const chartDom = document.getElementById('chart')
if (chartDom !== null) dispose(chartDom)
})
bindToolbar()
mountChart('/data/eurgbp_m1.csv')
+997
View File
@@ -0,0 +1,997 @@
{
"name": "kline-demo",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kline-demo",
"version": "1.0.0",
"dependencies": {
"klinecharts": "^10.0.2",
"vite": "^5.0.0"
},
"devDependencies": {}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@napi-rs/lzma-linux-x64-gnu": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz",
"integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^22.20 || ^24.12 || >=25"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz",
"integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz",
"integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz",
"integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz",
"integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz",
"integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz",
"integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz",
"integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==",
"cpu": [
"arm"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz",
"integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==",
"cpu": [
"arm"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz",
"integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==",
"cpu": [
"arm64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz",
"integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==",
"cpu": [
"arm64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz",
"integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==",
"cpu": [
"loong64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz",
"integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==",
"cpu": [
"loong64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz",
"integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==",
"cpu": [
"ppc64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz",
"integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==",
"cpu": [
"ppc64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz",
"integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==",
"cpu": [
"riscv64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz",
"integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==",
"cpu": [
"riscv64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz",
"integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==",
"cpu": [
"s390x"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz",
"integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==",
"cpu": [
"x64"
],
"libc": [
"glibc"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz",
"integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==",
"cpu": [
"x64"
],
"libc": [
"musl"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz",
"integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz",
"integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz",
"integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz",
"integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz",
"integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz",
"integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@types/estree": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
"integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/klinecharts": {
"version": "10.0.2",
"resolved": "https://registry.npmjs.org/klinecharts/-/klinecharts-10.0.2.tgz",
"integrity": "sha512-OJmaG047vd6RPKXxQAnbGPnfUQ3tKCKBlxk/oMvRJBXO6CyaZuqFGQ8ccoU6YTNew7DV8ynbsfti/JERe+M5IQ==",
"license": "Apache-2.0"
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
},
"node_modules/postcss": {
"version": "8.5.26",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.17",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/rollup": {
"version": "4.62.4",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz",
"integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.9"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@napi-rs/lzma-linux-x64-gnu": "1.5.1",
"@rollup/rollup-android-arm-eabi": "4.62.4",
"@rollup/rollup-android-arm64": "4.62.4",
"@rollup/rollup-darwin-arm64": "4.62.4",
"@rollup/rollup-darwin-x64": "4.62.4",
"@rollup/rollup-freebsd-arm64": "4.62.4",
"@rollup/rollup-freebsd-x64": "4.62.4",
"@rollup/rollup-linux-arm-gnueabihf": "4.62.4",
"@rollup/rollup-linux-arm-musleabihf": "4.62.4",
"@rollup/rollup-linux-arm64-gnu": "4.62.4",
"@rollup/rollup-linux-arm64-musl": "4.62.4",
"@rollup/rollup-linux-loong64-gnu": "4.62.4",
"@rollup/rollup-linux-loong64-musl": "4.62.4",
"@rollup/rollup-linux-ppc64-gnu": "4.62.4",
"@rollup/rollup-linux-ppc64-musl": "4.62.4",
"@rollup/rollup-linux-riscv64-gnu": "4.62.4",
"@rollup/rollup-linux-riscv64-musl": "4.62.4",
"@rollup/rollup-linux-s390x-gnu": "4.62.4",
"@rollup/rollup-linux-x64-gnu": "4.62.4",
"@rollup/rollup-linux-x64-musl": "4.62.4",
"@rollup/rollup-openbsd-x64": "4.62.4",
"@rollup/rollup-openharmony-arm64": "4.62.4",
"@rollup/rollup-win32-arm64-msvc": "4.62.4",
"@rollup/rollup-win32-ia32-msvc": "4.62.4",
"@rollup/rollup-win32-x64-gnu": "4.62.4",
"@rollup/rollup-win32-x64-msvc": "4.62.4",
"fsevents": "~2.3.2"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/vite": {
"version": "5.4.21",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
}
}
}
}
}
+15
View File
@@ -0,0 +1,15 @@
{
"name": "kline-demo",
"private": true,
"type": "module",
"version": "1.0.0",
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "vite build"
},
"dependencies": {
"klinecharts": "^10.0.2",
"vite": "^5.0.0"
},
"devDependencies": {}
}
+297
View File
@@ -0,0 +1,297 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html, body, #root {
width: 100%;
height: 100%;
font: 13px/1.4 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
overflow: hidden;
background: #ffffff;
color: #1f2328;
}
body.dark {
background: #0d1117;
color: #c9d1d9;
}
#toolbar {
display: flex;
align-items: center;
flex-wrap: nowrap;
gap: 10px;
height: 44px;
padding: 0 12px;
border-bottom: 1px solid #d0d7de;
background: #f6f8fa;
color: #1f2328;
white-space: nowrap;
}
body.dark #toolbar {
border-color: #30363d;
background: #161b22;
color: #c9d1d9;
}
#period-switcher,
#indicator-switcher,
#zoom-controls,
#tool-actions {
display: flex;
align-items: center;
gap: 2px;
padding: 2px;
border: 1px solid #d0d7de;
border-radius: 4px;
background: #ffffff;
}
body.dark #period-switcher,
body.dark #indicator-switcher,
body.dark #zoom-controls,
body.dark #tool-actions {
border-color: #30363d;
background: #21262d;
}
.toolbar-btn {
height: 28px;
padding: 0 10px;
border: 1px solid transparent;
border-radius: 4px;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
}
.toolbar-btn:hover {
background: #eef2f6;
border-color: #c9d0d8;
}
body.dark .toolbar-btn:hover {
background: #30363d;
border-color: #58a6ff;
}
.toolbar-btn.active {
border-color: #0969da;
background: #0969da;
color: #ffffff;
}
.toolbar-btn.active:hover {
background: #0969da;
}
.toolbar-btn.wide {
min-width: 42px;
}
.toolbar-btn.icon {
min-width: 32px;
font-size: 15px;
line-height: 1;
}
.toolbar-btn.active-icon {
color: #0969da;
background: transparent;
border-color: #0969da;
}
#file-picker {
display: flex;
align-items: center;
gap: 6px;
padding: 2px 6px;
border: 1px dashed #d0d7de;
border-radius: 4px;
background: #ffffff;
min-width: 180px;
max-width: 300px;
flex-shrink: 1;
}
#file-picker.over {
border-color: #0969da;
background: #ddf4ff;
}
body.dark #file-picker {
border-color: #30363d;
background: #21262d;
}
body.dark #file-picker.over {
border-color: #58a6ff;
background: #1f6feb;
}
#btn-file {
height: 28px;
padding: 0 10px;
border: none;
border-radius: 4px;
background: transparent;
cursor: pointer;
font-size: 14px;
line-height: 1;
color: inherit;
}
#btn-file:hover {
background: #eef2f6;
}
body.dark #btn-file:hover {
background: #30363d;
}
#file-name {
font-size: 12px;
color: #57606a;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
body.dark #file-name { color: #8b949e; }
#status {
margin-left: auto;
color: #57606a;
font-size: 12px;
}
body.dark #status { color: #8b949e; }
#chart {
width: 100%;
height: calc(100% - 44px);
}
.loading {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #8b949e;
font-size: 14px;
z-index: 999;
}
#crosshair-info {
position: fixed;
top: 52px;
right: 16px;
padding: 8px 12px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid #d0d7de;
font-size: 11px;
line-height: 1.7;
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
z-index: 100;
pointer-events: none;
min-width: 200px;
}
body.dark #crosshair-info {
background: rgba(22, 27, 34, 0.95);
border-color: #30363d;
color: #c9d1d9;
}
.hidden { display: none !important; }
#bar-detail,
#context-menu {
position: fixed;
z-index: 200;
border-radius: 6px;
border: 1px solid #d0d7de;
background: #ffffff;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
font-size: 12px;
line-height: 1.5;
}
body.dark #bar-detail,
body.dark #context-menu {
border-color: #30363d;
background: #161b22;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
}
#bar-detail {
padding: 10px 14px;
min-width: 160px;
}
#bar-detail .row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 1px 0;
}
#bar-detail .label { color: #8b949e; }
#bar-detail .value { font-weight: 600; font-family: ui-monospace, monospace; }
#bar-detail .bull { color: #cf222e; }
#bar-detail .bear { color: #1a7f37; }
body.dark #bar-detail .bull { color: #ff7b72; }
body.dark #bar-detail .bear { color: #7ee787; }
#context-menu {
padding: 4px 0;
min-width: 160px;
}
#context-menu .item {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
cursor: pointer;
color: inherit;
border: none;
background: none;
width: 100%;
font: inherit;
text-align: left;
}
#context-menu .item:hover {
background: #f6f8fa;
}
body.dark #context-menu .item:hover {
background: #30363d;
}
#context-menu .sep {
height: 1px;
margin: 4px 8px;
background: #d0d7de;
}
body.dark #context-menu .sep { background: #30363d; }
#context-menu .check { width: 14px; color: #0969da; }
.key-hint {
color: #8b949e;
font-size: 11px;
margin-left: 6px;
}