123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663 |
- /**
- * 发现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
- }
|