You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
406 lines
13 KiB
406 lines
13 KiB
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;
|