UNPKG

wuqy-component

Version:

这是一个微信小程序自定义组件库

632 lines (614 loc) 25.8 kB
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; }, }, });