项目中需要一个简单的折线图,引入echarts,但是打包后体积增大了很多差不多2M,觉得成本划不来,所以自己做了一个,效果也还行。
基本思想是利用canvas画出静态的坐标轴、数据点。提示tooltip是div,监听mousemove事件,获取clientX,
遍历数据点坐标数组(画静态坐标点的时候生成),找到当前client对应的点,translate到该点。
附上代码
<template>
<div style="height:250px">
<div class="canvas-wrapper">
<div class="tool-tip"
id="toolTip"
:content="tcontent"
v-show="toolTip.show"
:style="'transform:translateX('+cX+'px);top:'+ttp.top+'px'">
</div>
<canvas id="can">浏览器不支持canvas</canvas>
</div>
</div>
</template>
<script>
export default {
data () {
return {
xdata: [1, 2, 3], //纵坐标数据
ydata: [34565, 3455, 900007],//横坐标
toolTip: {
gw: 0,//x轴刻度之间距离
xPoints: [],//数据点的坐标
show: false,//是否显示提示
},
ttp: 0, //tooltip的y轴坐标
tcontent: '', //tooltip显示的内容
cX: 0, //toolTip定位坐标
}
},
watch: {//可以在此监听props,然后更新chart
},
//折线color、点color、提示color
mounted () {
const toolTip = document.getElementById('toolTip')
let start = new Date() //throttle
const ctx = document.getElementById('can')
this.initLine()
let _this = this
ctx.addEventListener('mousemove', (e) => {
_this.toolTip.show = true
if (new Date() - start > 500) {//throttle节流
start = new Date()
// console.log(e.clientX, e.clientY)
// console.log(_this.toolTip.xPoints)
let p = _this.toolTip.xPoints
let gw = _this.toolTip.gw / 2
for (let i = p.length - 1; i >= 0; i--) {
//遍历坐标点,找到该次事件对应的点
if (e.clientX > p[i].x - gw && e.clientX <= p[i].x + gw) {
_this.cX = p[i].x
_this.tcontent = _this.ydata[i] + '\n' + _this.xdata[i]
break
}
}
}
})
ctx.addEventListener('mouseleave', () => {
setTimeout(() => {
_this.toolTip.show = false
}, 2000);
})
},
methods: {
//初始化
initLine () {
let arr = this.ydata
let max = arr[0]
arr.forEach(e => {
max = Math.max(max, e)
})
const gap = this.getGap(max)
const len = Math.ceil(max / gap) + 1
const ctx = document.getElementById('can')
const wrapper = ctx.parentElement
//设置高度
let width = wrapper.offsetWidth //width
//document.getElementById('toolTip').style.top = wrapper.offsetTop + 'px'
//console.log(wrapper.offsetTop)
let height = wrapper.offsetHeight//height
let x = wrapper.offsetLeft
let y = wrapper.offsetTop
ctx.style.width = width + 'px'
ctx.style.height = height + 'px'
if (window.devicePixelRatio > 1) {
ctx.width = width * window.devicePixelRatio
ctx.height = height * window.devicePixelRatio
} else {
ctx.width = width
ctx.height = height
}
//绘坐标轴
this.draw(width, height, x, y, gap, len, this.xdata, arr)
let _this = this
new Promise(() => {
let wrapper = document.getElementById('can').parentElement
_this.ttp = wrapper.offsetTop
})
},
// 绘坐标轴
draw (width, height, x, y, gap, len, xdata, ydata) {
//预留10px 高度作为x轴的标注
const left = 0.5, bottom = height - 10.5, right = x + width
const e = document.getElementById('can')
const ctx = e.getContext("2d")
//devicePixelRatio物理像素分辨率与CSS像素分辨率的比率,移动端分辨率高会模糊
if (window.devicePixelRatio > 1) {
ctx.scale(window.devicePixelRatio, window.devicePixelRatio)
}
ctx.beginPath();
//y轴间隔高度
const gapHeight = parseInt(height / len)
//辅助横线
for (let i = height - gapHeight, j = 1; i >= 0 && j < len; i -= gapHeight, j++) {
ctx.moveTo(left + 30, i - 10.5)
ctx.fillText(j * gap, 15, i - 8.5) //绘制y轴标注
ctx.lineTo(right - 30, i - 10.5)
}
ctx.strokeStyle = 'rgba(0,0,0,.1)'
ctx.stroke()
ctx.closePath()
ctx.beginPath();
//绘制坐标轴
//y轴
ctx.moveTo(left + 29, gapHeight / 2)
ctx.lineTo(left + 29, bottom)
//x轴
this.toolTip.start = left + 30
ctx.moveTo(left + 30, bottom)
ctx.lineTo(right - 30, bottom)
const xlen = xdata.length
//x轴间隔
const gw = (width - 60) / xlen
this.toolTip.gw = gw
//x轴标注与刻度
for (let i = left + gw + 30, j = 0; j < xlen; j++ , i += gw) {
if (j + 1 < xlen) {//横线
ctx.moveTo(i, bottom)
ctx.lineTo(i, bottom + 3)
}
//
ctx.fillText(xdata[j], i - gw / 2 - 6, bottom + 10)
}
ctx.strokeStyle = "rgba(0,0,0,0.6)"
ctx.stroke()
ctx.beginPath()
let points = []
// 数据点
for (let i = 1; i <= xlen; i++) {
let yy = bottom - ydata[i - 1] * (gapHeight / gap)
let xx = left + 30 + i * gw - gw / 2
//ctx.moveTo(xx, yy)
ctx.beginPath()
ctx.arc(xx, yy, 2, 0, 2 * Math.PI);
ctx.fillStyle = "#fbc12a"
ctx.strokeStyle = '#fbc12a'
ctx.fill()
ctx.stroke()
// ctx.fillText(ydata[i - 1], xx - 5, yy - 10)
points.push({ x: parseFloat(xx), y: parseFloat(yy) })
}
this.toolTip.xPoints = points
ctx.closePath()
ctx.beginPath()
for (let i = xlen - 1; i > 0; i--) {
let cur = points[i]
if (i == 0) {
break
}
let next = points[i - 1]
ctx.moveTo(cur.x, cur.y)
ctx.lineTo(next.x, next.y)
}
ctx.lineJoin = 'round'
ctx.strokeStyle = 'rgba(251,193,42,0.6)'
ctx.lineWidth = 1
ctx.stroke()
},
// 获取y轴刻度间隔
getGap (num) {
if (!num) return 0
if (num <= 10) return num < 7 ? 1 : 2
if (num > 10 && num <= 100) return num < 70 ? 10 : 20
if (num > 100 && num <= 1000) return num < 700 ? 100 : 200
if (num > 1000 && num <= 10000) return num < 7000 ? 1000 : 2000
if (num > 10000 && num <= 100000) return num < 70000 ? 10000 : 20000
if (num > 100000 && num <= 1000000) return num < 700000 ? 100000 : 200000
if (num > 1000000 && num <= 10000000) return num < 7000000 ? 1000000 : 2000000
if (num > 10000000 && num <= 100000000) return num < 70000000 ? 10000000 : 20000000
},
}
}
</script>
<style lang="less" scoped>
.canvas-wrapper {
height: inherit;
width: 100%;
position: absolute;
z-index: inherit;
}
.tool-tip {
position: absolute;
z-index: 100000;
display: inline-block;
height: 220px;
width: 1px;
background: rgba(0, 0, 0, 0.5);
&::after {
content: attr(content);
color: #fbc12a;
word-break: break-all;
white-space: pre;
position: relative;
left: 10px;
top: 50px;
line-height: 20px;
border-radius: 4px;
display: inline-block;
min-width: 50px;
min-height: 25px;
background: rgba(0, 0, 0, 0.5);
}
}
</style>