tradingview这个k线图组件分轻量版和高级版,轻量版开源免费,高级版也是免费的,但是需要在官网提交表单申请(需要填写公司名称,地址,公司产品链接,license签字),高级版和轻量版的区别是高级版支持辅助线,图表作图,添加记号等高级功能。如果只是需要画k线图的话,那轻量版就完全够用。 轻量版tradingview/lightweight-charts的github链接:github.com/tradingview…
具体使用的代码,我就直接贴上我在工作项目里写的代码了,代码里的注释写的非常详细。
<!-- https://github.com/tradingview/lightweight-charts -->
<template>
<div class="wrapper" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.6)">
<div v-if="!isEmpty && !loading" id="kline-chart"></div>
<div v-else-if="isEmpty" style="height: 100%; display: flex; justify-content: center; align-items: center">
No K-line data available for the selected time period
</div>
</div>
</template>
<script setup lang="ts">
import { createChart } from 'lightweight-charts'
import { getRetifiveKLine } from '@/api/market'
import { getLanguageError, formatD } from '@/utils/util'
import dayjs from 'dayjs'
const props = defineProps({
stockCode: {
type: String,
default: '',
},
tabIndex: {
type: Number,
default: 0,
},
currentStock: {
type: Object,
default: {},
},
sourceType: {
type: Number,
default: 4,
},
// 图表类型, 默认蜡烛图, 可选line area等
chartType: {
type: String,
default: 'Candles',
},
})
const loading = ref(false) // loading加载动效
let isFetch = false // 是否正在加载时间范围数据, 避免用户向左拖拽图表时大量发送请求
const isEmpty = ref(false) // 后端没有返回k线数据时,需要显示文本
let histogramData = [] // 下面的交易量柱状图数据
let candleData = [] // 蜡烛图数据(默认)
let lineData = [] // 折线图数据(切换成line-chart用到)
let areaData = [] // 区块图数据(切换成area-chart用到)
let barData = [] // 柱状图数据(切换成bar-chart用到)
let histogramSeries = null // 下方交易量柱状图系列
let candlestickSeries = null // 蜡烛图系列
let lineSeries = null // 折线图系列
let areaSeries = null // 区块图系列
let barSeries = null // 柱状图系列
let endTime = dayjs() // 发送请求的endtime(每次新加载数据的最后一天)
let dateRangeLength = 30 // 发送请求的时间范围长度, 根据不同维护时间范围是不同的, 见resetParams(单位是天)
let resolution = 'H' // 分时图维度参数
let chart = null // 图表对象
onMounted(() => {
resetParams()
loading.value = true
refreshKLineData()
.then(() => {
setTimeout(() => {
drawChart()
})
})
.finally(() => {
loading.value = false
})
})
// 发送请求获取k线数据
const refreshKLineData = () => {
return new Promise((resolve, reject) => {
const params = {
symbol: props.stockCode,
resolution: resolution,
starttime: endTime.add(-dateRangeLength, 'd').format('YYYY-MM-DD'),
endtime: endTime.format('YYYY-MM-DD'),
}
getRetifiveKLine(params).then((response) => {
loading.value = false
// 获取k线失败的处理
if (response.status !== 0) {
ElMessage.error(getLanguageError('market.klinefail'))
reject(getLanguageError('market.klinefail'))
return
}
const resData = response.data
// 对于月k线和周k线, endTime只减去1天会出现数据重复的问题, 需要减掉1个月(周k线是1周)
if (props.tabIndex === 3) {
endTime = endTime.add(-dateRangeLength, 'd').add(-1, 'M')
}
if (props.tabIndex === 2) {
endTime = endTime.add(-dateRangeLength, 'd').add(-1, 'w')
} else {
endTime = endTime.add(-dateRangeLength - 1, 'd') // 最新时间更新, 等用户拖拽到左侧时以这个时间作为最新时间继续加载
}
isEmpty.value = resData.t.length === 0 && candleData.length === 0
if (isEmpty.value) {
reject('no data return')
return
}
// 由于后端传过来的数据是从新到旧, k线图组件的数据顺序必须是从旧到新, 所以做reverse处理
const sortedRes = {
c: resData.c.reverse(),
h: resData.h.reverse(),
l: resData.l.reverse(),
o: resData.o.reverse(),
v: resData.v.reverse(),
t: resData.t.reverse(),
}
// 为各个图表类型的数据分别赋上值
const newCandleData = []
const newHistogramData = []
const newLineData = []
const newAreaData = []
const newBarData = []
sortedRes.t.forEach((item, i) => {
newCandleData.push({
time: sortedRes.t[i],
open: sortedRes.o[i],
high: sortedRes.h[i],
low: sortedRes.l[i],
close: sortedRes.c[i],
volume: sortedRes.v[i],
})
newBarData.push({
time: sortedRes.t[i],
open: sortedRes.o[i],
high: sortedRes.h[i],
low: sortedRes.l[i],
close: sortedRes.c[i],
volume: sortedRes.v[i],
})
newHistogramData.push({
time: sortedRes.t[i],
value: sortedRes.v[i],
})
newLineData.push({
time: sortedRes.t[i],
value: sortedRes.c[i],
})
newAreaData.push({
time: sortedRes.t[i],
value: sortedRes.c[i],
})
})
candleData = [...newCandleData, ...candleData]
lineData = [...newLineData, ...lineData]
areaData = [...newAreaData, ...areaData]
barData = [...newBarData, ...barData]
histogramData = [...newHistogramData, ...histogramData]
resolve()
})
})
}
// 绘制k线图
const drawChart = () => {
const chartOptions = {
layout: { textColor: '#aaa', background: { type: 'solid', color: '#222' } }, // 文字颜色为白色, 背景为黑色
grid: { vertLines: { color: '#333' }, horzLines: { color: '#333' } }, // 横轴纵轴的分割线颜色
}
chart = createChart(document.getElementById('kline-chart'), chartOptions)
// 把当前类型的图表对象加载到图表中
setChartSeries(props.chartType)
setSeriesData(props.chartType)
// 下方交易量图
histogramSeries = chart.addHistogramSeries({
color: '#26A69A',
priceLineWidth: 0,
baseLineWidth: 0,
priceScaleId: 'left',
})
histogramSeries.setData(histogramData)
chart.applyOptions({
crosshair: {
// 隐藏鼠标悬浮时横纵轴默认显示的文字
vertLine: {
labelVisible: false,
},
},
timeScale: {
// format横轴时间显示
tickMarkFormatter: (time, index) => {
if (props.tabIndex === 0) {
return formatD(time * 1000, 'dd-MM hh:mm')
} else {
return formatD(time * 1000, 'dd-MM-yyyy')
}
},
},
})
chart.priceScale('left').applyOptions({
scaleMargins: {
top: 0.75, // 左侧是交易量的轴, 最高点距离顶部有75%的距离
bottom: 0,
},
})
chart.priceScale('right').applyOptions({
scaleMargins: {
top: 0.1, // 右侧是k线价格的轴, 最高点距离顶部有10%的距离
bottom: 0.35, // 最低点距离低部有35%的距离
},
})
// 组件没有自带tooltip, 按照官网教程实现, 代码较长用大括号包起来方便折叠
// https://tradingview.github.io/lightweight-charts/tutorials/how_to/tooltips#tracking-tooltip
{
const container = document.getElementById('kline-chart')
const toolTipWidth = 200 // tooltip宽度
const toolTipHeight = 200 // tooltip高度
const toolTipMargin = 0
// Create and style the tooltip html element
const toolTip = document.createElement('div')
toolTip.style = `width: ${toolTipWidth}px; height: ${toolTipHeight}px; position: absolute; display: none; padding: 8px; box-sizing: border-box; font-size: 12px; text-align: left; z-index: 1000; top: 12px; left: 12px; pointer-events: none; border: 1px solid; border-radius: 2px;font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;`
toolTip.style.background = 'white'
toolTip.style.color = 'black'
toolTip.style.borderColor = 'rgba( 38, 166, 154, 1)'
container.appendChild(toolTip)
// update tooltip
chart.subscribeCrosshairMove((param) => {
if (
param.point === undefined ||
!param.time ||
param.point.x < 0 ||
param.point.x > container.clientWidth ||
param.point.y < 0 ||
param.point.y > container.clientHeight
) {
toolTip.style.display = 'none'
} else {
toolTip.style.display = 'block'
const dataItem = candleData.find((item) => item.time === param.time)
const data = param.seriesData.get(getSeriesByChartType(props.chartType))
toolTip.innerHTML = `
<div style="color: ${'rgba( 38, 166, 154, 1)'}; font-size: 20px;">
${props.currentStock.stockName}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Opening: ${dataItem.open}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Closing: ${dataItem.close}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Highest: ${dataItem.high}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Lowest: ${dataItem.low}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Volume: ${dataItem.volume}
</div>
<div style="font-size: 16px; margin: 1px 0px; color: ${'black'}">
Time: ${
props.tabIndex === 0
? formatD(dataItem.time * 1000, 'dd-MM-yyyy hh:mm')
: formatD(dataItem.time * 1000, 'dd-MM-yyyy')
}
</div>`
const y = param.point.y
let left = param.point.x + toolTipMargin
if (left > container.clientWidth - toolTipWidth) {
left = param.point.x - toolTipMargin - toolTipWidth
}
let top = y + toolTipMargin
if (top > container.clientHeight - toolTipHeight) {
top = y - toolTipHeight - toolTipMargin
}
toolTip.style.left = left + 'px'
toolTip.style.top = top + 'px'
}
})
}
chart.timeScale().fitContent()
chart.timeScale().subscribeVisibleLogicalRangeChange((logicalRange) => {
// 最左侧的数据距离用户拖拽后的起点位置如果小于30个数据单位, 则发起请求加载更早的数据
if (logicalRange.from < 30 && !isFetch) {
// 加载更多数据
isFetch = true
refreshKLineData().then(() => {
setSeriesData(props.chartType)
histogramSeries.setData(histogramData)
isFetch = false
})
}
})
}
// 初始化solution和时间范围的参数
const resetParams = () => {
endTime = dayjs()
candleData = []
histogramData = []
switch (props.tabIndex) {
// 0代表小时分时图
case 0:
resolution = 'H'
dateRangeLength = 30
break
// 1代表天分时图
case 1:
resolution = 'D'
dateRangeLength = 120
break
// 2代表周分时图
case 2:
resolution = 'W'
dateRangeLength = 420
break
// 3代表月分时图
case 3:
resolution = 'M'
dateRangeLength = 1800
break
default:
break
}
}
// 根据图表类型获取图表集对象
const getSeriesByChartType = (chartType) => {
switch (chartType) {
case 'Candles':
return candlestickSeries
break
case 'Line':
return lineSeries
break
case 'Bars':
return barSeries
break
case 'Area':
return areaSeries
break
default:
break
}
}
// 加载图表
const setChartSeries = (chartType) => {
switch (chartType) {
case 'Candles':
candlestickSeries = chart.addCandlestickSeries({
upColor: '#26a69a',
downColor: '#ef5350',
borderVisible: false,
wickUpColor: '#26a69a',
wickDownColor: '#ef5350',
priceScaleId: 'right',
})
break
case 'Line':
lineSeries = chart.addLineSeries({
color: '#2962FF',
priceScaleId: 'right',
})
break
case 'Bars':
barSeries = chart.addBarSeries({
options: { upColor: '#26a69a', downColor: '#ef5350', priceScaleId: 'right' },
})
break
case 'Area':
areaSeries = chart.addAreaSeries({
options: {
lineColor: '#2962FF',
topColor: '#2962FF',
bottomColor: 'rgba(41, 98, 255, 0.28)',
priceScaleId: 'right',
},
})
break
default:
break
}
}
// 给图表系列设置数据
const setSeriesData = (chartType) => {
switch (chartType) {
case 'Candles':
candlestickSeries.setData(candleData)
break
case 'Line':
lineSeries.setData(lineData)
break
case 'Bars':
barSeries.setData(barData)
break
case 'Area':
areaSeries.setData(areaData)
break
default:
break
}
}
// 用户切换图表类型时, 需要卸载旧的series换上新的
watch(
() => props.chartType,
(newValue, oldValue) => {
if (chart) {
chart.removeSeries(getSeriesByChartType(oldValue))
setChartSeries(newValue)
setSeriesData(newValue)
}
}
)
</script>
<style lang="scss" scoped>
.wrapper {
width: 100%;
height: calc(100vh - 420px);
position: relative;
#kline-chart {
width: 100%;
height: calc(100vh - 420px);
position: relative;
}
}
</style>
另外我也研究过这个组件能否用在移动端上,首先lightweight-charts用在h5是完全没有问题的,代码和web端完全一致,直接使用即可。但是如果是用在uniapp的app端上的话,因为图表挂载到dom的写法在app端上是行不通的,所以直接在uniapp的vue文件里用lightweight-charts不行,不过可以通过web-view加载本地html的方式来实现。
web-view引入本地项目中的html文件,vue-cli创建的项目,web-view的html文件需要放在static目录下。
<web-view src="/static/hybrid/index.html"></web-view>
在html文件中引入js库画k线图。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"
/>
<title>k线图示例</title>
<style type="text/css">
body {
margin: 0;
}
#my-charts {
width: 100%;
height: 200px;
}
</style>
</head>
<body>
<div id="my-charts"></div>
<script type="text/javascript" src="./lightweight-charts.js"></script>
<script>
const chartOption = {
layout: { textColor: '#aaa', background: { type: 'solid', color: '#222' } }, // 文字颜色为白色, 背景为黑色
grid: { vertLines: { color: '#333' }, horzLines: { color: '#333' } }, // 横轴纵轴的分割线颜色
}
const chart = LightweightCharts.createChart(document.getElementById('my-charts'), chartOption)
const lineSeries = chart.addLineSeries({
color: '#2962FF',
})
lineSeries.setData([
{ time: '2019-04-01', value: 40.01 },
{ time: '2019-04-02', value: 96.63 },
{ time: '2019-04-03', value: 76.64 },
{ time: '2019-04-04', value: 21.89 },
{ time: '2019-04-05', value: 74.43 },
{ time: '2019-04-06', value: 80.01 },
{ time: '2019-04-07', value: 96.63 },
{ time: '2019-04-08', value: 46.64 },
{ time: '2019-04-09', value: 81.89 },
{ time: '2019-04-10', value: 74.43 },
{ time: '2019-04-11', value: 80.01 },
{ time: '2019-04-12', value: 46.63 },
{ time: '2019-04-13', value: 76.64 },
{ time: '2019-04-14', value: 81.89 },
{ time: '2019-04-15', value: 74.43 },
{ time: '2019-04-16', value: 80.01 },
{ time: '2019-04-17', value: 96.63 },
{ time: '2019-04-18', value: 26.64 },
{ time: '2019-04-19', value: 41.89 },
{ time: '2019-04-20', value: 74.43 },
{ time: '2019-04-21', value: 80.01 },
{ time: '2019-04-22', value: 176.63 },
{ time: '2019-04-23', value: 76.64 },
{ time: '2019-04-24', value: 11.89 },
{ time: '2019-04-25', value: 74.43 },
{ time: '2019-04-26', value: 90.01 },
{ time: '2019-04-27', value: 96.63 },
{ time: '2019-04-28', value: 76.64 },
{ time: '2019-04-29', value: 81.89 },
{ time: '2019-04-30', value: 123.43 },
])
chart.timeScale().fitContent()
</script>
</body>
</html>