/** * 发现bug * android app上字体要大于等于20px才能画多个字体,不然要每次调用fillText时都要设置字体 * android app上,重绘时会一直闪烁 * */ class canvasDraw { /* * @params{ DOM } canvasEl canvas元素节点 * @params{ Object } option 统计图配置信息 */ constructor(el, option) { // canvas DOM; this.option = option; this.context = el.context; // canvas宽高 this.width = el.width; this.height = el.height; // 预留XY文字间距 this.reservedSpace = 55; this.textAndDotSpace = 10; // X点之间的间隔 宽高 this.spaceX = 1; this.spaceY = 70; this.xWidth = 1; this.xHeight = 1; this.XList = []; this.YList = []; let yAxis = this.option.yAxis || {}; this.xAxis = this.option.xAxis.map(e => { let date = new Date(e) return date.getFullYear() + '年' + (date.getMonth() + 1) + '月' + date.getDate() + '日'; }) let measure = this.context.measureText(this.xAxis[0]).width; this.xAxisLength = measure || (this.width - this.reservedSpace) / 2; this.drawDataList = this.option.data || []; this.xScaleListLength = this.drawDataList[0] && this.drawDataList[0].data.length || 0; // 刻度数量 this.scale = yAxis.subsection ? yAxis.subsection + 1 : 5; this.xScaleSizeOriginal = 0; // XY文字颜色 X线条点颜色 数据线颜色 this.XYTextColor = '#666666'; this.XDotColor = '#666666'; this.dataLineXColor = this.option.dataLineXColor || []; if (this.dataLineXColor.length < this.xScaleListLength) { this.dataLineXColor = this.dataLineXColor.concat( [...Array(this.xScaleListLength - this.dataLineXColor.length).keys()] .map(e => this.getColor16()) ) } this.dataDotSize = 5; this.dataLineSize = 0.5; let data = this.drawDataList.length > 1 ? this.drawDataList.map(e => e.data).reduce((a,b) => a.concat(b || [])) : this.drawDataList[0].data; this.max = Math.max.apply(null, data); this.min = Math.min.apply(null, data); this.yScaleList = []; this.xScaleList = []; // 获取鼠标手势事件参数 this.touchX = 0; this.touchY = 0; // 计数的做优化的值 this.timeout = null; // 初始化 this.init(); // 绑定事件 if(el.addEventListener){ this.el.addEventListener('touchstart', touchstart, false); this.el.addEventListener('touchmove', touchmove, false); this.el.addEventListener('touchend', touchend, false); this.el.addEventListener('mouseout', mouseout, false); this.el.addEventListener('mousemove', mousemove, false); } } /** * 获取刻度数据 * @return {Array} kedu */ getDatas(){ let max = this.max; let keduObject = this.keduObject = this.getMeanInfo(); let kedu = []; if(keduObject.lose){ for (let i = keduObject.lose; i > 0; i--) { if(keduObject.mean < 0){ kedu.push(parseFloat((keduObject.mean * i).toFixed(2))); }else{ kedu.push(parseFloat('-' + (keduObject.mean * i).toFixed(2))); } } } kedu.push(0) for (let i = 1, length = keduObject.just; i <= length; i++) { kedu.push(parseFloat((Math.abs(keduObject.mean) * i).toFixed(2))); } return kedu } /** * 刻度算法 * @return {Number} 刻度大小 */ meanFun(max, num = 4, bool) { if(max == 0 || num == 0){ return 0 } if(max < 0){ Math.abs(max) } if (bool) { max = max.toFixed(2) * 100; return ((max + max % num) / num) / 100; } return Math.ceil((max + max % num) / num); } /** * 计算刻度数据 * @return {Object} .just 正刻度量 .lose 负刻度量 .mean刻度大小 */ getMeanInfo() { let max = this.max; let min = this.min; let subsection = this.scale - 1; let boolEan = Math.floor(max) === Math.floor(min); if (max > 0 && min < 0) { let copyMin = Math.abs(min); let mean = this.meanFun(copyMin + max, subsection, boolEan); let bool = copyMin > max; let num = Math.floor([bool ? copyMin : max][0] / mean); let nMean = this.meanFun([bool ? copyMin : max][0], num, boolEan); return { just: bool ? subsection - num : num, mean: nMean, lose: !bool ? subsection - num : num } } else if (max >= 0 && min >= 0) { return { just: subsection, mean: this.meanFun(max, subsection,boolEan), lose: 0 } } else if (max <= 0 && min < 0) { this.isLose = true; return { just: 0, mean: this.meanFun(min, subsection,boolEan), lose: subsection } } } /** * 初始化统计图 */ init() { this.YText = this.getDatas().reverse(); this.getYAxisList(); this.getXAxisList(); this.drawXYAxis(); this.drawYText(); this.drawXText(); this.initData(); } /** * 悬停线 */ drawMouseTooltipLine() { if (!this.isTouch()) return; this.dataLineSize = 1; // this.redraw(); this.drawMouseTooltipLineContext(); this.drawMouseTooltip(); } /** * 绘制悬停线 */ drawMouseTooltipLineContext(){ this.context.beginPath(); this.context.strokeStyle = 'rgba(0,0,0,0.3)'; this.context.lineTo(this.touchX, this.YList[0]); this.context.lineTo(this.touchX, this.YList[this.YList.length - 1]); this.context.stroke(); } /** * 用于判断是否是在图指定区域里 * @return {Boolean} true在指定区域里 */ isTouch() { let XList = this.XList; let YList = this.YList; let touchX = this.touchX; let touchY = this.touchY; let maxX = XList[XList.length - 1]; let minX = XList[0]; let maxY = YList[YList.length - 1]; let minY = YList[0]; return ( touchX >= minX && touchX <= maxX && touchY >= minY && touchY <= maxY ) } /** * 绘制悬停圈 * 绘制坐标点数据 */ drawMouseTooltip() { let XList = this.XList; let YList = this.YList; let touchX = this.touchX; let list = this.drawDataList; let fontTxtSize = this.fontTxtSize; let xScaleList = this.xScaleList; let yScaleList = this.yScaleList; let dataLineXColor = this.dataLineXColor; let lineHeight = this.lineHeight; let textIndent = this.textIndent; let minXValue = XList[0]; let minYValue = YList[0]; let xAxis = this.xAxis; let index = this.touchIndex = this.getBigValueMin(xScaleList, touchX); this.tooltipHeight = (fontTxtSize + lineHeight) * (list.length + 1) + lineHeight; list.forEach((e, i) => { this.context.beginPath(); if(touchX > xScaleList[index] - 2 && touchX < xScaleList[index] + 2){ this.context.fillStyle = dataLineXColor[i]; this.context.globalAlpha=0.3; this.context.arc(touchX, yScaleList[i][index], fontTxtSize / 2, 0, 2 * Math.PI); this.context.fill(); this.context.beginPath(); this.context.globalAlpha= 1; this.context.fillStyle = '#FFFFFF'; this.context.arc(touchX, yScaleList[i][index], fontTxtSize / 4 + 0.5, 0, 2 * Math.PI); this.context.fill(); this.context.beginPath(); this.context.globalAlpha= 1; this.context.fillStyle = dataLineXColor[i]; this.context.arc(touchX, yScaleList[i][index], fontTxtSize / 4, 0, 2 * Math.PI); this.context.fill(); } }) // tip长度 this.context.textAlign = 'start'; this.context.textBaseline = 'middle'; this.context.fillStyle = '#FFFFFF'; let measure = this.context.measureText(xAxis[index]).width; this.tooltipWidth = measure ? measure + this.fontTxtSize * 2 : (this.width - this.reservedSpace) / 2; list.forEach((e, i) => { let dataTxt = e.data; let title = (e.title || '') + ':' + dataTxt[index]; let measure = this.context.measureText(title).width; let tooltipWidth = measure ? measure + this.fontTxtSize * 2 : (this.width - this.reservedSpace) / 2; if(tooltipWidth > this.tooltipWidth){ this.tooltipWidth = tooltipWidth; } }) this.context.beginPath() this.context.fillStyle = 'rgba(255,255,255,1)'; this.roundRect(minXValue, minYValue, this.tooltipWidth, this.tooltipHeight, 3); this.context.fill(); this.context.beginPath() this.context.strokeStyle = 'rgba(220,65,55,1)'; this.context.moveTo(minXValue, minYValue); this.context.lineTo(minXValue + this.tooltipWidth, minYValue); this.context.lineTo(minXValue + this.tooltipWidth, minYValue + this.tooltipHeight); this.context.lineTo(minXValue, minYValue + this.tooltipHeight); this.context.closePath() this.context.stroke(); this.context.beginPath() this.context.fillStyle = '#333333'; this.context.font = this.fontTxtSize + this.fontFamily; this.context.fillText(xAxis[index], minXValue + textIndent, minYValue + lineHeight + textIndent / 2, ); let x = 0; let y = 0; list.forEach((e, i) => { this.context.beginPath(); this.context.fillStyle = '#333333'; let dataTxt = e.data; let title = (e.title || '') + ':' + dataTxt[index]; x = minXValue + textIndent; y = minYValue + (fontTxtSize + lineHeight) * (i + 1) + textIndent / 2; this.context.font = this.fontTxtSize + this.fontFamily; this.context.fillText(title, x + textIndent * 2, y + lineHeight); this.context.beginPath(); this.context.fillStyle = dataLineXColor[i]; this.context.arc(x + textIndent / 2, y + lineHeight, fontTxtSize / 4, 0, 2 * Math.PI); this.context.fill(); }) // #ifdef H5 setTimeout(() => { this.context.draw(true); },100) // #endif // #ifndef H5 this.context.draw(true); // #endif } /** * 绘制圆角矩形 */ roundRect(x, y, w, h, r) { if (w < 2 * r) r = w / 2; if (h < 2 * r) r = h / 2; this.context.beginPath(); this.context.moveTo(x + r, y); this.context.arcTo(x + w, y, x + w, y + h, r); this.context.arcTo(x + w, y + h, x, y + h, r); this.context.arcTo(x, y + h, x, y, r); this.context.arcTo(x, y, x + w, y, r); this.context.closePath(); } /** * 获取数据点下标 */ getBigValueMin(array, x) { for (let i = 0, length = array.length; i < length; i++) { if (array[i] >= x) { return i; } } return array.length - 1; } /** * 获取数据对应坐标 */ initData() { let YList = this.YList; let XList = this.XList; let YText = this.YText; let reservedSpace = this.reservedSpace; let xAxisLength = this.xAxisLength; // 刻度最大值最小值 let minData = YText[YText.length - 1]; let maxScaleList = YText[0]; // 0坐标到Y刻度总高度的距离 let XYHeight = YList[YList.length - 1]; // Y刻度总高度 let yHeight = XYHeight - this.spaceY; // x总刻度 let xScaleAll = XList[XList.length - 1]; // X每刻度大小 this.xScaleSizeOriginal = this.xScaleSize = parseFloat(((xScaleAll - reservedSpace) / (this.xScaleListLength - 1)).toFixed( 2)); // y刻度每份大小 let yScaleSize = this.yScaleSize = 0; // 当刻度最大值是0或者负数的时候 if (YText[0] <= 0) { this.yScaleSize = yScaleSize = parseFloat((yHeight / (Math.abs(minData) - Math.abs(maxScaleList))).toFixed(8)); this.drawDataList.forEach(data => { let list = data.data; this.yScaleList.push(list.map(e => Math.floor((Math.abs(e)) * yScaleSize + this.spaceY))); }) } else { this.yScaleSize = yScaleSize = parseFloat((yHeight / (maxScaleList - minData)).toFixed(8)); this.drawDataList.forEach(data => { let list = data.data; this.yScaleList.push(list.map(e => Math.floor((maxScaleList - e) * yScaleSize + this.spaceY))); }) } this.drawDataLine(); } /** * 重绘统计图 xy坐标轴 刻度文字 坐标线 统计线 */ redraw() { this.context.clearRect(0, 0, this.width, this.height); this.drawXYAxis(); this.drawYText(); this.drawDataLine(); } /** * 调节X坐标间距 * @param {String,Number} size = [1-10] */ handleXScaleSize(size) { this.xScaleSize = this.xScaleSizeOriginal * size; this.redraw(); } /** * 绘制统计线 */ drawDataLine() { let dataLineSize = this.dataLineSize; let dataDotSize = this.dataDotSize; let dataLineXColor = this.dataLineXColor; let reservedSpace = this.reservedSpace; let xScaleSize = this.xScaleSize; let YList = this.YList; let xScaleList = this.xScaleList = [...Array(this.xScaleListLength).keys()].map(i => Math.floor(i * xScaleSize + reservedSpace)); this.context.lineJoin = 'round'; let indexOf0 = this.YText.indexOf(0); let index0 = YList[indexOf0]; let drawDataList = this.drawDataList; let gradient = this.context .createLinearGradient(this.xScaleList[0], YList[0], this.xScaleList[0], YList[YList.length - 1]); drawDataList.forEach((e, i) => { if(e.isBg){ let bg = e.bgColor || [{ key:0, value: 'rgba(231, 0, 18, 0.5)' },{ key:1, value: 'rgba(231, 0, 18, 0.1)' }]; if(typeof bg === 'string'){ gradient = bg; }else{ bg.forEach((ee,ii) => { gradient.addColorStop(ee.key, ee.value); }) } } }) this.context.fillStyle = gradient; this.yScaleList.forEach((yD, yI) => { for (var i = 1, length = xScaleList.length; i < length; i++) { let xx = this.xScaleList[i - 1]; let xx1 = this.xScaleList[i]; let yy = yD[i - 1]; let yy1 = yD[i]; let ax = (xx1 - xx) / 3; let bx = xx + ax; let bx1 = xx + ax * 2; this.context.beginPath(); this.context.moveTo(xx, yy); this.context.bezierCurveTo(bx, yy, bx1, yy1, xx1, yy1); if(drawDataList[yI].isBg){ this.context.lineTo(xx1, index0) this.context.lineTo(xx, index0) this.context.lineTo(xx, yy) this.context.fill(); } } }) this.context.lineWidth = this.dataLineSize; this.yScaleList.forEach((yD, yI) => { this.context.beginPath(); this.context.strokeStyle = dataLineXColor[yI]; for (var i = 1, length = xScaleList.length; i < length; i++) { let xx = this.xScaleList[i - 1]; let xx1 = this.xScaleList[i]; let yy = yD[i - 1]; let yy1 = yD[i]; let ax = (xx1 - xx) / 3; let bx = xx + ax; let bx1 = xx + ax * 2; this.context.moveTo(xx, yy); this.context.bezierCurveTo(bx, yy, bx1, yy1, xx1, yy1); } this.context.stroke(); }) // #ifdef H5 setTimeout(() => { this.context.draw(true); },100) // #endif // #ifndef H5 this.context.draw(true); // #endif } /** * 随机生成16位字符颜色 */ getColor16() { return '#' + Math.random().toString(16).slice(-6); } /** * 绘制Y刻度文字 */ drawYText() { let YText = this.YText; let YList = this.YList; let XYTextColor = this.XYTextColor; let reservedSpace = this.reservedSpace; let textAndDotSpace = this.textAndDotSpace; let x = reservedSpace - textAndDotSpace; this.context.fillStyle = XYTextColor; this.context.textAlign = 'end'; this.context.textBaseline = 'middle'; YList.forEach((y, i) => { this.context.font = this.fontTxtSize + this.fontFamily; this.context.fillText(this.YText[i], x, y, x); }) this.drawXText(); } /** * 绘制X刻度文字,由于时间问题,这里只绘制了起始和结束文字 */ drawXText() { let xAxis = this.xAxis; let YList = this.YList; let XList = this.XList; let xAxisLength = this.xAxisLength; let XYTextColor = this.XYTextColor; let reservedSpace = this.reservedSpace; this.context.fillStyle = XYTextColor; this.context.textAlign = 'start'; this.context.textBaseline = 'middle'; this.context.font = this.fontTxtSize + this.fontFamily; this.context.fillText(xAxis[0], XList[0], YList[YList.length - 1] + this.fontTxtSize, xAxisLength + this.fontTxtSize * 4); this.context.font = this.fontTxtSize + this.fontFamily; this.context.textAlign = 'end'; this.context.fillText(xAxis[xAxis.length - 1], XList[XList.length - 1], YList[YList.length - 1] + this.fontTxtSize, xAxisLength + this.fontTxtSize * 4); // #ifdef H5 setTimeout(() => { this.context.draw(true); },100) // #endif // #ifndef H5 this.context.draw(true); // #endif } /** * 绘制XY统计图雏形 */ drawXYAxis() { let XList = this.XList; let YList = this.YList; let xWidth = this.xWidth; let xHeight = this.xHeight; let XDotColor = this.XDotColor; let reservedSpace = this.reservedSpace; this.context.fillStyle = XDotColor; YList.forEach((y, i) => { XList.forEach((x, j) => { this.context.fillRect(x, y, xWidth, xHeight); }) }) // #ifdef H5 setTimeout(() => { this.context.draw(true); },100) // #endif // #ifndef H5 this.context.draw(true); // #endif } /** * 获取X轴对应坐标点 */ getXAxisList() { let width = this.width; let spaceX = this.spaceX; let xWidth = this.xWidth; let xHeight = this.xHeight; let reservedSpace = this.reservedSpace; let length = (width - reservedSpace - this.fontTxtSize) / (xWidth + spaceX); for (let i = 0; i < length; i++) { this.XList.push(i * (spaceX + xWidth) + reservedSpace); } } /** * 获取y轴对应坐标点 */ getYAxisList() { let height = this.height; let scale = this.scale; let reservedSpace = this.reservedSpace; this.spaceY = Math.floor((height - reservedSpace) / scale); for (let i = 0; i < scale; i++) { this.YList.push(i * this.spaceY + this.spaceY); } this.setTextStyle(); } /** * 设置字体类型,行高,字体大小,缩进 */ setTextStyle() { // 字体 this.fontFamily = 'px 宋体'; this.lineHeight = 10; this.textIndent = 10; this.fontTxtSize = this.spaceY / 5 < 14 ? 14 : this.spaceY / 5; } }; let touchstart = function (e) { if (!e.touches[0]) return this.ctx.touchX = e.touches[0].x; this.ctx.touchY = e.touches[0].y; this.ctx.drawMouseTooltipLine() } let touchmove = function (e) { if (!e.touches[0]) return this.ctx.touchX = e.touches[0].x; this.ctx.touchY = e.touches[0].y; this.ctx.drawMouseTooltipLine() } let mousemove = function (e){ if (e.pageX){ this.ctx.touchX = e.x; this.ctx.touchY = e.y; this.ctx.drawMouseTooltipLine() } } let mouseout = function (e) { this.ctx.dataLineSize = 0.5; this.ctx.redraw() } let touchend = function (e) { this.ctx.dataLineSize = 0.5; this.ctx.redraw() } export default { canvasDraw, touchstart, touchmove, mousemove, mouseout, touchend }