const utils = require('./util'); const STYLE_DEFAULT = { TEXT_ALIGN: 'center', COLOR: '#000000', LIGHT_COLOR: '#fff', FONT_SIZE: 24, AXLE_COLOR: '#e5e5e5' }; function CurvePainter(ctx, config) { this.ctx = ctx; this.windowWidth = config.windowWidth; this.canvasWidth = config.canvasWidth; this.canvasHeight = config.canvasHeight; this.marginTop = config.marginTop; // 画布上边留白 this.marginBottom = config.marginBottom; // 画布底边留白 this.marginLeft = config.marginLeft; // 画布左边留白 this.marginRight = config.marginRight; // 画布右边留白 this.drawWidth = this.canvasWidth - this.marginLeft - this.marginRight; // 绘制宽度 this.drawHeight = this.canvasHeight - this.marginTop - this.marginBottom; // 绘制高度 } CurvePainter.prototype = { setData(data) { this.hAxleData = data.hAxleData || []; // x轴刻度 this.hAxleTitle = data.hAxleTitle || ''; // x轴标题 this.vAxleData = data.vAxleData || []; // y轴刻度 this.vAxleTitle = data.vAxleTitle || ''; // y轴标题 this.originDataList = data.dataList || []; // 原始数据集 this.selectedIndex = data.selectedIndex; // 被选中的序号 this.reference = data.reference || 0; // 参考值 this.unit = data.unit || ''; // 数值单位 this.drawHAxle = data.drawHAxle || false; // 是否绘制横轴 this.drawVAxle = data.drawVAxle || false; // 是否绘制纵轴 this.drawCurveDecorate = data.drawCurveDecorate || false; // 是否绘制背景 this.drawCurveDataPoint = data.drawCurveDataPoint || false; // 是否绘制数据点 this.drawCurveDataText = data.drawCurveDataText || false; // 是否绘制数据值 }, setOffset(data) { this.topOffset = data.topOffset; // 最大数据点距画布顶部距离 this.bottomOffset = data.bottomOffset; // 最小数据点距画布底部距离 }, setStyle(config) { this.curveWidth = config.curveWidth || this.toPx(6); // 曲线宽度 this.curveColor = config.curveColor || STYLE_DEFAULT.COLOR; // 曲线颜色 this.curveDecorateColors = config.curveDecorateColors || [STYLE_DEFAULT.LIGHT_COLOR, STYLE_DEFAULT.LIGHT_COLOR]; // 背景颜色 this.scaleColor = config.scaleColor || STYLE_DEFAULT.COLOR; // 刻度字体颜色 this.selectedScaleColor = config.selectedScaleColor || STYLE_DEFAULT.COLOR; // 选中刻度的字体颜色 this.scaleFontSize = config.scaleFontSize || this.toPx(STYLE_DEFAULT.FONT_SIZE); // 刻度字体大小 this.selectedScaleFontSize = config.selectedScaleFontSize || this.toPx(STYLE_DEFAULT.FONT_SIZE); // 选中的刻度字体大小 this.dataPointFontColor = config.dataPointFontColor || STYLE_DEFAULT.COLOR; // 数据值颜色 this.selectedDataPointFontColor = config.selectedDataPointFontColor || STYLE_DEFAULT.COLOR; // 选中的数据值颜色 this.dataPointFontSize = config.dataPointFontSize || this.toPx(STYLE_DEFAULT.FONT_SIZE); // 数据值字体大小 this.selectedDataPointFontSize = config.selectedDataPointFontSize || this.toPx(STYLE_DEFAULT.FONT_SIZE); // 选中的数据值字体大小 this.textAlign = config.textAlign || 'center'; // 文字对其方式 this.hAxleTitleColor = config.hAxleTitleColor || STYLE_DEFAULT.COLOR; // x轴标题颜色 this.vAxleTitleColor = config.vAxleTitleColor || STYLE_DEFAULT.COLOR; // y轴标题颜色 this.hAxleColor = config.hAxleColor || STYLE_DEFAULT.AXLE_COLOR; // x轴线颜色 this.hAxleWidth = config.hAxleWidth || 1; // x轴宽度 this.vAxleColor = config.vAxleColor || STYLE_DEFAULT.AXLE_COLOR; // y轴颜色 this.vAxleWidth = config.vAxleWidth || 1; // y轴宽度 this.pointMarginColor = config.pointMarginColor || STYLE_DEFAULT.COLOR; // 数据点外圆颜色 this.pointMarginRadius = config.pointMarginRadius || this.toPx(24 / 2); // 数据点外缘半径 this.pointColor = config.pointColor || STYLE_DEFAULT.COLOR;// 数据点颜色 this.pointRadius = config.pointRadius || this.toPx(12 / 2); // 数据点半径 }, addAction(fn) { if (!utils.isFunction(fn)) return; if (!this.actions) { this.actions = []; } this.actions.push(fn); }, prepare() { let self = this; let data = this.originDataList; dataToPos(); function dataToPos() { if (!self.isValid(data)) { return; } let {max, min} = self.getMaxAndMin(data, self.reference); let range = max !== min ? max - min : 1; // 需要考虑最大值等于最小值,也就是所有值都一样的情况 let drawWidth = self.drawWidth; let drawHeight = self.drawHeight; let dataPointMaxHeight = drawHeight - self.topOffset - self.bottomOffset; // 数据点最大高度 let xOffset = drawWidth / (data.length - 1); let calcData = []; for (let i = 0; i < data.length; i++) { if (data[i]) { calcData.push({ x: i * xOffset, y: self.topOffset + dataPointMaxHeight - self.calcRate(data, max, min, range, i) * dataPointMaxHeight, value: data[i], }); } else { calcData.push({ value: data[i] }); } } self.calcData = calcData; } return this; }, // 数据合法化处理 calcRate(list, max, min, range, idx) { if(max === min) { return 1; } let diff = list[idx] - min; if (diff === 0) { return 0; } else { if (range === 0) { return list[idx] / max; } else { return diff / range; } } }, getMaxAndMin(list, target) { let max = 0, min = 0, self = this; let listWithoutZero = this.filterZero(list); if (listWithoutZero.length > 1) { return { max: self.max(listWithoutZero), min: self.minWithoutZero(listWithoutZero) }; } else { let only = listWithoutZero[0]; max = only > target ? only : target; min = only > target ? target: only; return { max, min }; } }, max(obj) { if (!this.isValid(obj)) { return void 0; } let max = obj[0]; obj.forEach(item => { if (item > max) { max = item; } }); return max; }, minWithoutZero(obj) { if (!this.isValid(obj)) { return void 0; } let min = obj[0]; obj.forEach(item => { if (item > 0 && item <= min) { min = item; } }); return min; }, filterZero(list) { return list.filter(item => item); }, draw(reDraw) { reDraw && this.clear(); this.performDraw(); }, performDraw() { // 坐标轴变换方便计算 this.resetCurvesCoordinate(); // 绘制轴线 this.drawAxle(); // 绘制曲线 this.drawCurve(); this.ctx.draw(); }, clear() { this.ctx && this.ctx.clearRect(0, 0, this.drawHeight, this.drawHeight); }, resetCurvesCoordinate() { let xCut = this.marginLeft; let yCut = this.marginTop; this.ctx.translate(xCut, yCut); }, drawAxle() { let ctx = this.ctx; let drawWidth = this.drawWidth; let drawHeight = this.drawHeight; let defaultColor = STYLE_DEFAULT.COLOR; let drawHAxle = this.drawHAxle; if (drawHAxle) { let hAxleData = this.hAxleData; let dataLen = hAxleData.length; let selectedIndex = this.selectedIndex; let selectedScaleColor = this.selectedScaleColor; let scaleFontSize = this.scaleFontSize; let selectedScaleFontSize = this.selectedScaleFontSize; let hAxleTitle = this.hAxleTitle; let hAxleTitleColor = this.hAxleTitleColor; let hAxleWidth = this.hAxleWidth; let hAxleColor = this.hAxleColor; // 绘制刻度 ctx.setFontSize(scaleFontSize); ctx.setTextAlign(this.textAlign); let item; let xOffset = dataLen - 1 ? drawWidth / (dataLen - 1) : 0; for (let i = 0, len = dataLen; i < len; i++) { item = hAxleData[i]; ctx.setFillStyle(i === selectedIndex ? selectedScaleColor : defaultColor); ctx.setFontSize(i === selectedIndex ? selectedScaleFontSize : scaleFontSize); ctx.fillText(item, xOffset * i, drawHeight); } // 绘制刻度标题 ctx.setFillStyle(hAxleTitleColor); ctx.setFontSize(scaleFontSize); ctx.fillText(hAxleTitle, drawWidth + this.toPx(45), drawHeight); // 45 = 偏离y轴的距离 // 画轴线 ctx.setLineWidth(hAxleWidth); ctx.setStrokeStyle(hAxleColor); ctx.beginPath(); ctx.moveTo(0, drawHeight - this.toPx(36)); // 36 = 刻度文字与x轴的距离 + 文字大小 ctx.lineTo(drawWidth + this.toPx(55), drawHeight - this.toPx(36)); ctx.stroke(); } }, drawCurve() { let self = this; let ctx = this.ctx; let drawWidth = this.drawWidth; let drawHeight = this.drawHeight; let calcData = this.calcData; let selectedIndex = self.selectedIndex; if (calcData && calcData.length) { let curveWidth = this.curveWidth; let curveColor = this.curveColor; ctx.setLineWidth(curveWidth); let pos, lastPos = calcData[0]; ctx.beginPath(); ctx.moveTo(lastPos.x, lastPos.y); for (let i = 1, len = calcData.length; i < len; i++) { pos = calcData[i]; if (!pos.value) continue; // 这里每次都得创建一个新的渐变 // FIXME 这里是为了解决,android,在同一个canvas内, // FIXME 先用gradient绘制了线条后,无法setStrokeStyle设置为普通颜色的Bug let gd = ctx.createLinearGradient(0, 0, pos.x, pos.y); gd.addColorStop(0, curveColor); gd.addColorStop(1, curveColor); ctx.setStrokeStyle(gd); if (i === 1) { ctx.lineTo(pos.x, pos.y); } else { ctx.beginPath(); ctx.moveTo(lastPos.x, lastPos.y); ctx.lineTo(pos.x, pos.y); } ctx.stroke(); lastPos = pos; } drawCurveDecorate(); // 绘制曲线修饰 drawCurveDataPoint(); // 绘制数据点 drawCurveDataText(); // 绘制数据值 drawCustom(); // 绘制自定义内容 } function drawCurveDecorate() { let drawCurveDecorate = self.drawCurveDecorate; if (!drawCurveDecorate || calcData.length <= 1) return; let curveDecorateColors = self.curveDecorateColors; let lg = ctx.createLinearGradient(0, 0, 0, drawHeight); lg.addColorStop(0, curveDecorateColors[0]); lg.addColorStop(1, curveDecorateColors[1]); ctx.setFillStyle(lg); ctx.beginPath(); ctx.moveTo(calcData[0].x || 0, calcData[0].y || 0); for(let i = 0, len = calcData.length; i < len; i++) { let pos = calcData[i]; if (!pos.value) continue; ctx.lineTo(pos.x, pos.y); } ctx.lineTo(getNonZeroPos(calcData).x, drawHeight); ctx.lineTo(calcData[0].x || 0, drawHeight); ctx.lineTo(calcData[0].x || 0, calcData[0].y || 0); ctx.closePath(); ctx.fill(); } function getNonZeroPos(list) { let len = list.length; for (let i = len - 1, end = 0; i >= end; i--) { let temp = list[i]; if (!temp.value) continue; return temp; } } function drawCurveDataPoint() { let drawCurveDataPoint = self.drawCurveDataPoint; if (!drawCurveDataPoint) return; let pointMarginColor = self.pointMarginColor; let pointColor = self.pointColor; for (let i = 0, len = calcData.length; i < len; i++) { let pos = calcData[i]; if (!pos.value) continue; // 被选中的数据点有外边框 if (i === selectedIndex) { ctx.setFillStyle(pointMarginColor); ctx.beginPath(); ctx.arc(pos.x, pos.y, self.toPx(24 / 2), 0, 2 * Math.PI); ctx.fill(); } // 再画颜色小圆 ctx.setFillStyle(pointColor); ctx.beginPath(); ctx.arc(pos.x, pos.y, self.toPx(12 / 2), 0, 2 * Math.PI); ctx.fill(); } } function drawCurveDataText() { let drawCurveDataText = self.drawCurveDataText; if (!drawCurveDataText) return; let textAlign = self.textAlign; let dataPointFontSize = self.dataPointFontSize; let selectedDataPointFontSize = self.selectedDataPointFontSize; let dataPointFontColor = self.dataPointFontColor; let selectedDataPointFontColor = self.selectedDataPointFontColor; ctx.setTextAlign(textAlign); for (let i = 0, len = calcData.length; i < len; i++) { let pos = calcData[i]; if (!pos.value) continue; ctx.setFontSize( i === selectedIndex ? selectedDataPointFontSize : dataPointFontSize); ctx.setFillStyle( i === selectedIndex ? selectedDataPointFontColor : dataPointFontColor); ctx.fillText( `${pos.value.toFixed(1)}`, pos.x, pos.y - self.toPx(25)); // 25 = 数据点和数据文字的距离 } } function drawCustom() { let actions = self.actions; actions && actions.forEach(action => { action.apply(self); }); } }, toPx(rpx) { let windowWidth = this.windowWidth; return utils.rpx_to_px(rpx, windowWidth); }, isValid(obj) { let isArray = utils.isArray(obj); if (isArray) { return obj.length > 0; } else { return isArray; } } }; module.exports = CurvePainter;