wuqy-component
Version:
这是一个微信小程序自定义组件库
632 lines (614 loc) • 25.8 kB
JavaScript
const screenWidth = wx.getWindowInfo().screenWidth;
const canvasLattice = (screenWidth * (350 / 375)) / 7;
let indexView = 0;
function _getTestDate() {
const arr = [];
for (let i = 1; i <= 30; i++) {
arr.push(i + "日");
}
return arr;
}
Component({
/**
* 使用组件前特别注意:必传属性有data(数组嵌套数组)、Date(数组)
* 使用示例:<charts data="[[1,2,3],[12,13,14]]" Date="["1日","2日","3日"]"/>
* @param data array 数据
* @param option 为折线(或曲线)的配置。
* color为折线或曲线的颜色(string)。
* pointColor为折线或曲线的点的颜色(string)。
* pointSize为折线或曲线的点的大小(number)。
* radianCurve true为曲线,false为折线。
* setLineDash(array):设置虚线,默认实线,例如:[1,5],实线和虚线的比例为1:5。
* excursion,为控制点的偏移量 默认值globalOption.canvasLattice * 0.4;如果globalOption.canvasLattice为50,则默认值为20。
* upperLimit 数值的上限,number。(((不支持修改)))
* floorLimit 数值的下限,number。(((不支持修改)))
* @param globalOption 为全局设置。
* color为参考线(虚线)的颜色(string)。
* xyAxisHide(boolean)为x和y的坐标轴是否显示。
* yReferenceLineHide和xReferenceLineHide(boolean)分别为x和y轴的虚线参考线,true隐藏,false显示。
* axisColor为坐标轴的颜色(string)。
* setLineDash(array):设置虚线,默认实线,例如:[1,5],实线和虚线的比例为1:5。
* canvasWidth,(((不支持修改)))为画布的宽度,默认为350px,px是画布中的单位,画布中是省略的。
* canvasHeight,(((不支持修改)))画布的高度,默认为250px。
* canvasPadding_left(((不支持修改))),为画布的左内边距,设置好合适的内边距方便添加文字内容。
* canvasPadding_bottom(((不支持修改))),为画布的下边距。
* canvasPadding_top(((不支持修改))),为画布的上边距。
* canvasLattice 正方形格子的边长,(((不支持修改))),自动计算。
*/
properties: {
//图表字体大小
fontSize: {
type: Number,
value: 26,
},
// x和y轴文字的颜色
xycolor: {
type: String,
value: "rgba(255, 255, 255, 0.9)",
},
cell: {
type: Object,
value: { y: "档", unit: "单位:元" },
},
//数据
data: {
type: Array,
value: [
[100, -201, 100, 200, 100, 200, 100],
[200, 100, 200, 100, 200, 100, 200],
],
},
//日期
Date: {
type: Array,
value: _getTestDate(),
},
option: {
type: Array,
value: [
{
upperLimit: undefined, //自动计算,无需手动修改
floorLimit: undefined, //自动计算,无需手动修改
radianCurve: true,
color: "red",
pointColor: "white",
pointSize: 3,
setLineDash: [],
excursion: 20,
},
{
upperLimit: undefined,
floorLimit: undefined,
radianCurve: true,
color: "#17B9FF",
pointColor: "white",
pointSize: 3,
setLineDash: [],
excursion: 20,
},
],
},
globalOption: {
type: Object,
value: {
color: "#17B9FF",
xyAxisHide: false,
yReferenceLineHide: false,
xReferenceLineHide: false,
axisColor: "rgba(230, 230, 230, 1)",
setLineDash: [2, 5],
canvasWidth: screenWidth * (350 / 375), //自动计算,无需手动修改
canvasHeight: screenWidth * (1 / 375) * 250, //自动计算,无需手动修改
canvasLattice: canvasLattice, //自动计算,无需手动修改
canvasPadding_left: canvasLattice * 0.4, // 尽量不要修改,请在css文件中设置
canvasPadding_top: canvasLattice * 0.2, // 尽量不要修改,请在css文件中设置
canvasPadding_bottom: canvasLattice * 0.4, // 尽量不要修改,请在css文件中设置
},
},
},
data: {
// 四条水平线对应的数据;
yAxisData: [],
// y轴四条水平线的名称;
yAxisBoundaryName: [],
//[ ]1.4
intoView: 0,
canvasHeight: screenWidth * (1 / 375) * 250,
canvasWidth: screenWidth * (350 / 375),
scrollHeight: screenWidth * (1 / 375) * 250,
scrollWidht: screenWidth * (350 / 375),
},
//数据监听更新是一个成功回调,某个特定的数据更新了,就会调用此函数。
observers: {
data: function (data) {
this.updateChartsData(data);
},
},
methods: {
swiperItemClick() {
this.switchDetailData();
},
change(e) {
this.showData(e.detail.current);
},
dotClick(e) {
let index = e.currentTarget.dataset;
this.setData({
[`dotInfo[${index.outerIndex}][${index.innerIndex}].hide`]: !this.data
.dotInfo[index.outerIndex][index.innerIndex].hide,
});
setTimeout(() => {
this.setData({
[`dotInfo[${index.outerIndex}][${index.innerIndex}].hide`]: !this.data
.dotInfo[index.outerIndex][index.innerIndex].hide,
});
}, 1000);
},
//模拟数据调试专用
async buttonClick() {
const newData = [];
for (let i = 0; i < Math.random() * 30; i++) {
newData.push(Math.floor(Math.random() * 200));
}
const newData2 = [];
for (let i = 0; i < Math.random() * 30; i++) {
newData2.push(Math.floor(Math.random() * 300));
}
this.initCanvasStatus = await this.updateChartsData([newData, newData2]);
indexView = 0;
this.setData({
intoView: indexView,
});
},
/**
* @param select 1表示第一条,2表示第二条,以此类推。当为Boolean值时,false会全部展示,true会全部隐藏。
*/
async showData(...select) {
const dotInfo = await this.initCanvasStatus;
if (typeof select[0] === "boolean") {
const outerNewDotInfo = dotInfo.map((item) =>
item.map((v) => ({ ...v, hide: select[0] }))
);
this.setData({
dotInfo: outerNewDotInfo,
});
} else {
const outerNewDotInfo = [];
select.forEach((item) => {
//select中的元素比index大1;
item--;
if (item == -1) return;
if (item < dotInfo.length) {
const newDotInfo = dotInfo[item].map((v) => ({
...v,
hide: false,
}));
outerNewDotInfo[item] = newDotInfo;
}
});
this.setData({
dotInfo: outerNewDotInfo,
});
}
},
// 获取各个数组中的最大值和最小值,推算出四个档位的值,
_getEachArrayMaxMinValue(data = this.data.data) {
const outerCollection = [];
data.forEach((item) => {
const maxValue = item.reduce(
(accumulator, currentValue) => Math.max(accumulator, currentValue),
item[0]
);
//获取上限
const autoUpperLimit = Math.ceil(maxValue / 40) * 40;
//使用正无穷Infinity或item[0]其实都一样,只不过第一次比较是同一个元素,即item[0]。
const minValue = item.reduce(
(accumulator, currentValue) => Math.min(accumulator, currentValue),
Infinity
);
//获取下限,即使是负值,也能正确获取
let autoFloorLimit = Math.floor(minValue / 40) * 40;
if (autoFloorLimit > 0) autoFloorLimit = 0;
//上限和下限绝对值之和
const absoluteValueSum =
Math.abs(autoFloorLimit) + Math.abs(autoUpperLimit);
if (autoUpperLimit >= 10000 || autoFloorLimit < -1000) {
this.setData({
fontSize: 20,
});
}
const innerCollection = [];
for (
let i = autoUpperLimit;
i >= autoFloorLimit;
i -= absoluteValueSum / 4
) {
innerCollection.push(i);
}
outerCollection.push(innerCollection);
});
return outerCollection;
},
// 切换展示详细数据
switchDetailData() {
indexView++;
if (indexView > this.data.yAxisData.length) indexView = 0;
this.setData(
{
intoView: indexView,
},
() => this.showData(indexView)
);
return indexView;
},
},
lifetimes: {
// 在组件实例进入页面节点树时执行
attached: function () {
const updateChartsData = (
data = this.data.data,
option = this.data.option,
globalOption = this.data.globalOption
) => {
return new Promise((resolve) => {
// 收集时间轴的位置信息;
const timeAxis = [];
// 获取数据的最长长度
const maxLength = data.reduce(
(accumulator, currentValue) =>
Math.max(accumulator, currentValue.length),
-Infinity
);
// 最长的那个数组
const longestArray = data.reduce((before, current) => {
return current.length > before.length ? current : before;
}, data[0]);
const _globalOption = {
color: "#17B9FF",
xyAxisHide: false,
yReferenceLineHide: false,
xReferenceLineHide: false,
axisColor: "white",
setLineDash: [2, 5],
canvasWidth: screenWidth * (350 / 375), //增加的宽度一定要是50的倍数
canvasHeight: screenWidth * (1 / 375) * 250, //高度的控制还需要调试//[ ]1.3
//正方形格子的宽高
canvasLattice: canvasLattice,
canvasPadding_left: canvasLattice * 0.4, // 尽量不要修改,请在css文件中设置
canvasPadding_top: canvasLattice * 0.2, // 尽量不要修改,请在css文件中设置
canvasPadding_bottom: canvasLattice * 0.4,
};
//如果用户没有把所有的属性都定义,只定义了部分,已经定义的会覆盖默认值,未定义的会使用默认值。
globalOption = {
..._globalOption,
...globalOption,
};
globalOption.canvasWidth =
maxLength > 7
? (maxLength - 7) * globalOption.canvasLattice +
screenWidth * (350 / 375)
: screenWidth * (350 / 375);
longestArray.forEach((v, i) => {
timeAxis.push({
left:
i * globalOption.canvasLattice +
globalOption.canvasPadding_left,
top:
globalOption.canvasHeight - globalOption.canvasPadding_bottom,
content: this.data.Date[i],
});
});
const exec = () => {
return new Promise((resolve) => {
// 收集圆点的位置和value信息;
const dataCollector = Array();
this.createSelectorQuery()
.select("#myCanvas") // 在 WXML 中填入的 id
.fields({ node: true, size: true })
.exec((res) => {
// Canvas 对象
const canvas = res[0].node;
// 渲染上下文
const ctx = canvas.getContext("2d");
this.ctx = ctx;
// Canvas 画布的实际绘制宽高
const width = res[0].width;
const height = res[0].height;
// 初始化画布大小
const dpr = wx.getWindowInfo().pixelRatio;
canvas.width = width * dpr;
canvas.height = height * dpr;
ctx.scale(dpr, dpr);
// 清空画布,为数据更新做准备
ctx.clearRect(0, 0, width, height);
const heightMinusBottomPadding =
globalOption.canvasHeight -
globalOption.canvasPadding_bottom;
//水平参考线
ctx.strokeStyle = globalOption.color;
ctx.setLineDash(globalOption.setLineDash);
if (!globalOption.xReferenceLineHide) {
let ladder = -1;
for (
let i = heightMinusBottomPadding;
i > 0;
i -= globalOption.canvasLattice
) {
/*eslint-disable*/
ladder++;
ctx.beginPath();
ctx.moveTo(globalOption.canvasPadding_left, i);
ctx.lineTo(
globalOption.canvasWidth -
(globalOption.canvasLattice -
globalOption.canvasPadding_left),
i
);
ctx.stroke();
}
}
//垂直参考线
if (!globalOption.yReferenceLineHide) {
for (
let i = globalOption.canvasPadding_left;
i < globalOption.canvasWidth;
i += globalOption.canvasLattice
) {
ctx.beginPath();
ctx.moveTo(i, globalOption.canvasPadding_bottom);
ctx.lineTo(i, heightMinusBottomPadding);
ctx.stroke();
}
}
ctx.strokeStyle = globalOption.axisColor;
ctx.setLineDash([]);
if (!globalOption.xyAxisHide) {
// Y轴
ctx.beginPath();
ctx.moveTo(
globalOption.canvasPadding_left,
globalOption.canvasPadding_top
);
ctx.lineTo(
globalOption.canvasPadding_left,
heightMinusBottomPadding
);
ctx.stroke();
// Y轴箭头
ctx.beginPath();
ctx.moveTo(
globalOption.canvasPadding_left,
globalOption.canvasPadding_top
);
ctx.lineTo(
globalOption.canvasPadding_left - 5,
globalOption.canvasPadding_top + 5
);
ctx.lineTo(
globalOption.canvasPadding_left,
globalOption.canvasPadding_top - 10
);
ctx.lineTo(
globalOption.canvasPadding_left + 5,
globalOption.canvasPadding_top + 5
);
ctx.lineTo(
globalOption.canvasPadding_left,
globalOption.canvasPadding_top
);
ctx.fillStyle = globalOption.axisColor;
ctx.fill();
ctx.stroke();
// x轴坐标
ctx.beginPath();
ctx.moveTo(
globalOption.canvasPadding_left,
heightMinusBottomPadding
);
ctx.lineTo(
globalOption.canvasWidth,
heightMinusBottomPadding
);
ctx.stroke();
// X轴箭头
ctx.beginPath();
ctx.moveTo(
globalOption.canvasWidth - 10,
heightMinusBottomPadding
);
ctx.lineTo(
globalOption.canvasWidth - 15,
heightMinusBottomPadding + 5
);
ctx.lineTo(
globalOption.canvasWidth,
heightMinusBottomPadding
);
ctx.lineTo(
globalOption.canvasWidth - 15,
heightMinusBottomPadding - 5
);
ctx.fillStyle = globalOption.axisColor;
ctx.fill();
ctx.stroke();
}
// 这里的形参中使用了默认值,在最外层的封装函数中,即使在option对象中仅设置部分属性的值,也能顺利的展示。
const chartsData = (
data,
radianCurve,
color,
pointColor,
pointSize,
setLineDash,
excursion,
absoluteValueSum,
floorLimit
) => {
const oldData = data;
data = data.map(
(v) =>
globalOption.canvasLattice *
4 *
(Math.abs(floorLimit - v) / absoluteValueSum)
);
ctx.strokeStyle = color;
ctx.setLineDash(setLineDash); //ctx.setLineDash([]),实线。
// 画曲线
ctx.beginPath();
for (
let i = 0;
i < (data.length > 7 ? data.length : 7);
i++
) {
if (!radianCurve) {
ctx.lineTo(
i * globalOption.canvasLattice +
globalOption.canvasPadding_left,
heightMinusBottomPadding - data[i]
);
} else {
const currentX =
i * globalOption.canvasLattice +
globalOption.canvasPadding_left,
currentY = heightMinusBottomPadding - data[i];
const nextPointX =
(i + 1) * globalOption.canvasLattice +
globalOption.canvasPadding_left,
nextPointY = heightMinusBottomPadding - data[i + 1];
//控制点的偏移量
// const excursion = 20;
// 第一个控制点
const x1 = currentX + excursion,
y1 = currentY;
// 第二个控制点
const x2 = nextPointX - excursion,
y2 = nextPointY;
if (i == 0) {
// 惊奇的发现了把两个控制点合并为一个点,会变成一条垂直的直线,再把一个目标Y值设置和currentY一致,垂直直线变成一个点,也就是表格的第一个点。
ctx.bezierCurveTo(
globalOption.canvasPadding_left,
currentY,
globalOption.canvasPadding_left,
currentY,
currentX,
currentY
);
ctx.bezierCurveTo(
x1,
y1,
x2,
y2,
nextPointX,
nextPointY
);
} else {
ctx.bezierCurveTo(
x1,
y1,
x2,
y2,
nextPointX,
nextPointY
);
}
}
}
ctx.stroke();
// 画圆点,把这些圆点的位置信息暴露出去,加上数据信息,在css中使用这些位置信息,实现通过点击获取各个圆点的具体数据。
const innerDataCollector = Array();
data.forEach((v, i) => {
ctx.beginPath();
ctx.arc(
i * globalOption.canvasLattice +
globalOption.canvasPadding_left,
heightMinusBottomPadding - v,
pointSize,
0,
Math.PI * 2,
false
); //如果是半圆的话,false或true为显示多的那部分,true为显示少的那部分。
ctx.fillStyle = pointColor;
ctx.fill();
innerDataCollector.push({
left:
i * globalOption.canvasLattice +
globalOption.canvasPadding_left,
top: heightMinusBottomPadding - v,
value: oldData[i],
hide: true,
color: color,
});
});
return innerDataCollector;
};
const yAxisData = this._getEachArrayMaxMinValue(data);
for (let i = 0; i < data.length; i++) {
const yData = yAxisData[i];
// absoluteValueSum是上限和下限的绝对值之和
const absoluteValueSum =
Math.abs(yData[0]) + Math.abs(yData[yData.length - 1]);
//floorLimit是下限
const floorLimit = yData[yData.length - 1];
// 形参option其实是this.data.option对象的引用,储存的是对象的地址。所以在修改upperLimit的时候,其实直接操作的是this.data.option对象。
// option[i].upperLimit = autoUpperLimit;
option[i] = option.propertyIsEnumerable(i) ? option[i] : {};
const getDotData = chartsData(
data[i],
option[i].hasOwnProperty("radianCurve")
? option[i].radianCurve
: true,
option[i].color ? option[i].color : "#17B9FF",
option[i].pointColor ? option[i].pointColor : "white",
option[i].pointSize ? option[i].pointSize : 3,
option[i].setLineDash ? option[i].setLineDash : [],
option[i].excursion
? option[i].excursion
: globalOption.canvasLattice * 0.4,
option[i].upperLimit
? option[i].upperLimit
: absoluteValueSum,
option[i].floorLimit ? option[i].floorLimit : floorLimit
);
dataCollector.push(getDotData);
}
const yAxisBoundaryName = [];
for (let i = 4; i >= 0; i--) {
yAxisBoundaryName.push(`${i + this.data.cell.y}`);
}
this.setData(
{
dotInfo: dataCollector,
yAxisBoundaryName,
timeAxis,
yAxisData,
},
() => {
resolve(dataCollector);
}
);
});
});
};
//!!! this.setData函数是一个异步操作,在更新数据的时候有时差,会导致 this.createSelectorQuery函数选择组件节点的时候没有获取到重新渲染出来的组件节点,而是仍然获取上一个组件节点的信息,导致设置的globalOption.canvasWidth和canvas.width的数据不相等,导致表格压缩或拉伸变形。
this.setData(
{
canvasWidth:
maxLength > 7
? (maxLength - 7) * globalOption.canvasLattice +
screenWidth * (350 / 375)
: screenWidth * (350 / 375),
},
() => {
//这里传递dataCollector是exec中setData成功回调中resolve函数的返回值,其目的是同步setData完成的时间,因为setData完成了,才能读取,然后再更新一次。这里为什么要等待设置好了宽高才调用exec函数,因为设置好了宽高画布才不会出现拉扯变形。
exec.call(this).then((dataCollector) => {
resolve(dataCollector);
});
}
);
});
};
this.initCanvasStatus = updateChartsData();
this.updateChartsData = updateChartsData;
},
},
});