744 lines
25 KiB
JavaScript
Raw Normal View History

2025-05-08 09:16:37 +08:00
/*
* qiun-data-charts 秋云高性能跨全端图表组件
* Copyright (c) 2021 QIUN® 秋云 https://www.ucharts.cn All rights reserved.
* Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
* 复制使用请保留本段注释感谢支持开源
* 为方便更多开发者使用如有更好的建议请提交码云 Pull Requests
*
* uCharts®官方网站
* https://www.uCharts.cn
*
* 开源地址:
* https://gitee.com/uCharts/uCharts
*
* uni-app插件市场地址
* http://ext.dcloud.net.cn/plugin?id=271
*
*/
import uCharts from 'u-charts.js';
import cfu from 'config-ucharts.js';
function deepCloneAssign(origin = {}, ...args) {
for (let i in args) {
for (let key in args[i]) {
if (args[i].hasOwnProperty(key)) {
origin[key] = args[i][key] && typeof args[i][key] === 'object' ? deepCloneAssign(Array.isArray(args[i][key]) ? [] : {}, origin[key], args[i][key]) : args[i][key];
}
}
}
return origin;
}
function formatterAssign(args,formatter) {
for (let key in args) {
if(args.hasOwnProperty(key) && args[key] !== null && typeof args[key] === 'object'){
formatterAssign(args[key],formatter)
}else if(key === 'format' && typeof args[key] === 'string'){
args['formatter'] = formatter[args[key]] ? formatter[args[key]] : undefined;
}
}
return args;
}
function debounce(fn, wait) {
let timer = false;
return function() {
clearTimeout(timer);
timer && clearTimeout(timer);
timer = setTimeout(() => {
timer = false;
fn.apply(this, arguments);
}, wait);
};
}
var lastMoveTime = null;
var moveLength = 0;
Component({
options: {
pureDataPattern: /^_/
},
properties: {
type: {
type: String,
value: null
},
canvasId: {
type: String,
value: 'uchartsid'
},
canvas2d: {
type: Boolean,
value: false
},
background: {
type: String,
value: 'rgba(0,0,0,0)'
},
animation: {
type: Boolean,
value: true
},
chartData: {
type: Object,
value: {
categories: [],
series: []
}
},
localdata:{
type: Array,
value: []
},
opts: {
type: Object,
value: {}
},
loadingType: {
type: Number,
value: 2
},
errorShow: {
type: Boolean,
value: true
},
errorReload: {
type: Boolean,
value: true
},
errorMessage: {
type: String,
value: null
},
inScrollView: {
type: Boolean,
value: false
},
reshow: {
type: Boolean,
value: false
},
reload: {
type: Boolean,
value: false
},
disableScroll: {
type: Boolean,
value: false
},
optsWatch: {
type: Boolean,
value: true
},
onzoom: {
type: Boolean,
value: false
},
ontap: {
type: Boolean,
value: true
},
ontouch: {
type: Boolean,
value: false
},
onmovetip: {
type: Boolean,
value: false
},
tooltipShow: {
type: Boolean,
value: true
},
tooltipFormat: {
type: String,
value: undefined
},
tooltipCustom: {
type: Object,
value: undefined
},
pageScrollTop: {
type: Number,
value: 0
},
tapLegend: {
type: Boolean,
value: true
}
},
data: {
cid: 'uchartsid',
type2d: true,
cWidth: 375,
cHeight: 250,
showchart: false,
mixinDatacomErrorMessage:null,
mixinDatacomLoading:true,
_inWin: false,
_pixel: 1,
_drawData:{},
_lastDrawTime:null
},
observers: {
'chartData.**': function(val) {
if (typeof val === 'object') {
this._clearChart();
if (val.series && val.series.length > 0) {
this.beforeInit();
}else{
let mixinDatacomLoading = true;
let showchart = false;
let mixinDatacomErrorMessage = null;
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
}
} else {
let mixinDatacomLoading = false;
let showchart = false;
let mixinDatacomErrorMessage = '参数错误chartData数据类型错误';
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
}
},
'localdata': function(val) {
if (val.length > 0) {
this.beforeInit();
}else{
let mixinDatacomLoading = true;
this._clearChart();
let showchart = false;
let mixinDatacomErrorMessage = null;
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
}
},
'opts.**': function(val) {
if (typeof val === 'object') {
if (this.data.optsWatch == true) {
this.checkData(this.data._drawData);
}
} else {
let mixinDatacomLoading = false;
let showchart = false;
let mixinDatacomErrorMessage = '参数错误opts数据类型错误';
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
}
},
'reshow': function(val) {
if (val === true && this.data.mixinDatacomLoading === false) {
setTimeout(() => {
let mixinDatacomErrorMessage = null;
this.setData({ mixinDatacomErrorMessage });
this.checkData(this.data._drawData);
}, 200);
}
},
'reload': function(val) {
if (val === true) {
let showchart = false;
let mixinDatacomErrorMessage = null;
this.setData({ showchart, mixinDatacomErrorMessage });
this.reloading();
}
},
'mixinDatacomErrorMessage': function(val) {
if (val) {
this.emitMsg({name: 'error', params: {type:"error", errorShow: this.data.errorShow, msg: val, id: this.data.cid}});
if(this.data.errorShow){
console.log('[秋云图表组件]' + val);
}
}
},
'errorMessage': function(val) {
if (val && this.data.errorShow && val !== null && val !== 'null' && val !== '') {
let mixinDatacomLoading = false;
let showchart = false;
let mixinDatacomErrorMessage = val;
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
} else {
let showchart = false;
let mixinDatacomErrorMessage = null;
this.setData({ showchart, mixinDatacomErrorMessage });
this.reloading();
}
}
},
lifetimes: {
attached: function () {
let cid = this.data.canvasId;
if (this.data.canvasId == 'uchartsid' || this.data.canvasId == '') {
let t = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
let len = t.length;
let id = '';
for (let i = 0; i < 32; i++) {
id += t.charAt(Math.floor(Math.random() * len));
}
cid = id;
}
let _inWin = false;
const systemInfo = wx.getSystemInfoSync();
if(systemInfo.platform === 'windows' || systemInfo.platform === 'mac'){
_inWin = true;
}
let type2d = false;
let _pixel = 1;
if (this.data.canvas2d === false || systemInfo.platform === 'windows' || systemInfo.platform === 'mac') {
type2d = false;
}else{
type2d = true;
_pixel = systemInfo.pixelRatio;
}
this.setData({ cid, type2d, _pixel });
},
ready: function () {
wx.nextTick(() => {
this.beforeInit();
})
},
detached: function () {
delete cfu.option[this.data.cid]
delete cfu.instance[this.data.cid]
},
},
pageLifetimes: {
show: function() {
// 页面被展示可以选择执行重绘参考下面resize的方法
},
hide: function() {
// 页面被隐藏
},
resize: function(size) {
const _this = this;
debounce(function(res) {
if (_this.data.mixinDatacomLoading == true) {
return;
}
const errmsg = _this.data.mixinDatacomErrorMessage;
if (errmsg !== null && errmsg !== 'null' && errmsg !== '') {
return;
}
_this.resizeHandler();
}, 200)
}
},
methods: {
beforeInit(){
this.data.mixinDatacomErrorMessage = null;
if (typeof this.data.chartData === 'object' && this.data.chartData != null && this.data.chartData.series !== undefined && this.data.chartData.series.length > 0) {
//拷贝一下chartData为了opts变更后统一数据来源
let _drawData = deepCloneAssign({}, this.data.chartData);
let mixinDatacomLoading = false;
let showchart = true;
this.setData({ _drawData, mixinDatacomLoading, showchart });
this.checkData(this.data.chartData);
}else if(this.data.localdata.length>0){
let mixinDatacomLoading = false;
let showchart = true;
this.setData({ mixinDatacomLoading, showchart });
this.localdataInit(this.data.localdata);
}else{
let mixinDatacomLoading = true;
this.setData({ mixinDatacomLoading });
}
},
localdataInit(resdata){
let needCategories = false;
let tmpData = {categories:[], series:[]};
let tmpcategories = [];
let tmpseries = [];
//拼接categories
needCategories = cfu.categories.includes(this.data.type);
if(needCategories === true){
//如果props中的chartData带有categories则优先使用chartData的categories
if(this.data.chartData && this.data.chartData.categories && this.data.chartData.categories.length>0){
tmpcategories = this.data.chartData.categories
}else{
let tempckey = {};
resdata.map(function(item, index) {
if (item.text != undefined && !tempckey[item.text]) {
tmpcategories.push(item.text)
tempckey[item.text] = true
}
});
}
tmpData.categories = tmpcategories
}
//拼接series
let tempskey = {};
resdata.map(function(item, index) {
if (item.group != undefined && !tempskey[item.group]) {
tmpseries.push({ name: item.group, data: [] });
tempskey[item.group] = true;
}
});
//如果没有获取到分组名称(可能是带categories的数据也可能是不带的饼图类)
if (tmpseries.length == 0) {
tmpseries = [{ name: '默认分组', data: [] }];
//如果是需要categories的图表类型
if(needCategories === true){
for (let j = 0; j < tmpcategories.length; j++) {
let seriesdata = 0;
for (let i = 0; i < resdata.length; i++) {
if (resdata[i].text == tmpcategories[j]) {
seriesdata = resdata[i].value;
}
}
tmpseries[0].data.push(seriesdata);
}
//如果是饼图类的图表类型
}else{
for (let i = 0; i < resdata.length; i++) {
tmpseries[0].data.push({"name": resdata[i].text,"value": resdata[i].value});
}
}
//如果有分组名
} else {
for (let k = 0; k < tmpseries.length; k++) {
//如果有categories
if (tmpcategories.length > 0) {
for (let j = 0; j < tmpcategories.length; j++) {
let seriesdata = 0;
for (let i = 0; i < resdata.length; i++) {
if (tmpseries[k].name == resdata[i].group && resdata[i].text == tmpcategories[j]) {
seriesdata = resdata[i].value;
}
}
tmpseries[k].data.push(seriesdata);
}
//如果传了group而没有传text即没有categories正常情况下这种数据是不符合数据要求规范的
} else {
for (let i = 0; i < resdata.length; i++) {
if (tmpseries[k].name == resdata[i].group) {
tmpseries[k].data.push(resdata[i].value);
}
}
}
}
}
tmpData.series = tmpseries
//拷贝一下chartData为了opts变更后统一数据来源
let _drawData = deepCloneAssign({}, tmpData);
this.setData({ _drawData });
this.checkData(tmpData)
},
_clearChart() {
let cid = this.data.cid
if (cfu.option[cid] && cfu.option[cid].context) {
const ctx = cfu.option[cid].context;
if(typeof ctx === "object" && !cfu.option[cid].update){
ctx.clearRect(0, 0, this.data.cWidth, this.data.cHeight);
ctx.draw();
}
}
},
reloading() {
if(this.data.errorReload === false){
return;
}
let showchart = false;
let mixinDatacomErrorMessage = null;
this.setData({ showchart, mixinDatacomErrorMessage });
this.beforeInit();
},
checkData(anyData) {
let cid = this.data.cid
//复位opts或eopts
if (this.data.type && cfu.type.includes(this.data.type)) {
cfu.option[cid] = deepCloneAssign({}, cfu[this.data.type], this.data.opts);
cfu.option[cid].canvasId = cid;
} else {
let mixinDatacomLoading = false;
let showchart = false;
let mixinDatacomErrorMessage = '参数错误props参数中type类型不正确';
this.setData({ mixinDatacomLoading, showchart, mixinDatacomErrorMessage });
}
//挂载categories和series
let newData = deepCloneAssign({}, anyData);
if (newData.series !== undefined && newData.series.length > 0) {
let mixinDatacomErrorMessage = null;
this.setData({ mixinDatacomErrorMessage });
cfu.option[cid].categories = newData.categories;
cfu.option[cid].series = newData.series;
wx.nextTick(()=>{
this.init()
})
}
},
resizeHandler() {
//渲染防抖
let currTime = Date.now();
let lastDrawTime = this.data._lastDrawTime?this.data._lastDrawTime:currTime-3000;
let duration = currTime - lastDrawTime;
if (duration < 1000) return;
let chartdom = wx.createSelectorQuery().in(this)
.select('#boxid'+this.data.cid)
.boundingClientRect(data => {
let showchart = true;
this.setData({ showchart });
if (data.width > 0 && data.height > 0) {
if (data.width !== this.data.cWidth || data.height !== this.data.cHeight) {
this.checkData(this.data._drawData)
}
}
})
.exec();
},
init() {
let cid = this.data.cid
let chartdom = wx.createSelectorQuery().in(this)
.select('#boxid'+cid)
.boundingClientRect(data => {
if (data.width > 0 && data.height > 0) {
let mixinDatacomLoading = false;
let showchart = true;
let _lastDrawTime = Date.now();
let cWidth = data.width;
let cHeight = data.height;
let mixinDatacomErrorMessage = null;
this.setData({ mixinDatacomLoading, showchart, _lastDrawTime, cWidth, cHeight, mixinDatacomErrorMessage });
cfu.option[cid].background = this.data.background == 'rgba(0,0,0,0)' ? '#FFFFFF' : this.data.background;
cfu.option[cid].canvas2d = this.data.type2d;
cfu.option[cid].pixelRatio = this.data._pixel;
cfu.option[cid].animation = this.data.animation;
cfu.option[cid].width = data.width * this.data._pixel;
cfu.option[cid].height = data.height * this.data._pixel;
cfu.option[cid].ontap = this.data.ontap;
cfu.option[cid].ontouch = this.data.ontouch;
cfu.option[cid].onmovetip = this.data.onmovetip;
cfu.option[cid].tooltipShow = this.data.tooltipShow;
cfu.option[cid].tooltipFormat = this.data.tooltipFormat;
cfu.option[cid].tooltipCustom = this.data.tooltipCustom;
cfu.option[cid].inScrollView = this.data.inScrollView;
cfu.option[cid].lastDrawTime = this.data._lastDrawTime;
cfu.option[cid].tapLegend = this.data.tapLegend;
cfu.option[cid] = formatterAssign(cfu.option[cid],cfu.formatter)
if (this.data.type2d === true) {
const query = wx.createSelectorQuery().in(this)
query
.select('#' + cid)
.fields({ node: true, size: true })
.exec(res => {
if (res[0]) {
const canvas = res[0].node;
const ctx = canvas.getContext('2d');
cfu.option[cid].context = ctx;
cfu.option[cid].rotateLock = cfu.option[cid].rotate;
if(cfu.instance[cid] && cfu.option[cid] && cfu.option[cid].update === true){
this._updataUChart(cid)
}else{
canvas.width = data.width * this.data._pixel;
canvas.height = data.height * this.data._pixel;
canvas._width = data.width * this.data._pixel;
canvas._height = data.height * this.data._pixel;
setTimeout(()=>{
cfu.option[cid].context.restore();
cfu.option[cid].context.save();
this._newChart(cid)
},100)
}
} else {
this.data.showchart = false;
this.data.mixinDatacomErrorMessage = '参数错误开启2d模式后未获取到dom节点canvas-id:' + cid;
}
});
} else {
cfu.option[cid].context = wx.createCanvasContext(cid, this);
if(cfu.instance[cid] && cfu.option[cid] && cfu.option[cid].update === true){
this._updataUChart(cid)
}else{
setTimeout(()=>{
cfu.option[cid].context.restore();
cfu.option[cid].context.save();
this._newChart(cid)
},100)
}
}
} else {
let mixinDatacomLoading = false;
let showchart = false;
this.setData({ mixinDatacomLoading, showchart });
if (this.data.reshow == true) {
this.data.mixinDatacomErrorMessage = '布局错误未获取到父元素宽高尺寸canvas-id:' + cid;
}
}
})
.exec();
},
saveImage(){
wx.canvasToTempFilePath({
canvasId: this.data.cid,
success: res=>{
wx.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: function () {
wx.showToast({
title: '保存成功',
duration: 2000
});
}
});
}
},this);
},
_newChart(cid) {
if (this.data.mixinDatacomLoading == true) {
return;
}
let showchart = true;
this.setData({ showchart });
cfu.instance[cid] = new uCharts(cfu.option[cid]);
cfu.instance[cid].addEventListener('renderComplete', () => {
this.emitMsg({name: 'complete', params: {type:"complete", complete: true, id: cid}});
cfu.instance[cid].delEventListener('renderComplete')
});
cfu.instance[cid].addEventListener('scrollLeft', () => {
this.emitMsg({name: 'scrollLeft', params: {type:"scrollLeft", scrollLeft: true, id: cid}});
});
cfu.instance[cid].addEventListener('scrollRight', () => {
this.emitMsg({name: 'scrollRight', params: {type:"scrollRight", scrollRight: true, id: cid}});
});
},
_updataUChart(cid) {
cfu.instance[cid].updateData(cfu.option[cid])
},
_tooltipDefault(item, category, index, opts) {
if (category) {
let data = item.data
if(typeof item.data === "object"){
data = item.data.value
}
return category + ' ' + item.name + ':' + data;
} else {
if (item.properties && item.properties.name) {
return item.properties.name;
} else {
return item.name + ':' + item.data;
}
}
},
_showTooltip(e) {
let cid = this.data.cid
let tc = cfu.option[cid].tooltipCustom
if (tc && tc !== undefined && tc !== null) {
let offset = undefined;
if (tc.x >= 0 && tc.y >= 0) {
offset = { x: tc.x, y: tc.y + 10 };
}
cfu.instance[cid].showToolTip(e, {
index: tc.index,
offset: offset,
textList: tc.textList,
formatter: (item, category, index, opts) => {
if (typeof cfu.option[cid].tooltipFormat === 'string' && cfu.formatter[cfu.option[cid].tooltipFormat]) {
return cfu.formatter[cfu.option[cid].tooltipFormat](item, category, index, opts);
} else {
return this._tooltipDefault(item, category, index, opts);
}
}
});
} else {
cfu.instance[cid].showToolTip(e, {
formatter: (item, category, index, opts) => {
if (typeof cfu.option[cid].tooltipFormat === 'string' && cfu.formatter[cfu.option[cid].tooltipFormat]) {
return cfu.formatter[cfu.option[cid].tooltipFormat](item, category, index, opts);
} else {
return this._tooltipDefault(item, category, index, opts);
}
}
});
}
},
_tap(e,move) {
let cid = this.data.cid
let currentIndex = null;
let legendIndex = null;
if (this.data.inScrollView === true) {
let chartdom = wx.createSelectorQuery().in(this)
.select('#boxid'+cid)
.boundingClientRect(data => {
e.changedTouches=[];
e.changedTouches.unshift({ x: e.detail.x - data.left, y: e.detail.y - data.top - this.data.pageScrollTop});
if(move){
if (this.data.tooltipShow === true) {
this._showTooltip(e);
}
}else{
currentIndex = cfu.instance[cid].getCurrentDataIndex(e);
legendIndex = cfu.instance[cid].getLegendDataIndex(e);
if(this.data.tapLegend === true){
cfu.instance[cid].touchLegend(e);
}
if (this.data.tooltipShow === true) {
this._showTooltip(e);
}
this.emitMsg({name: 'getIndex', params: { type:"getIndex", event:{ x: e.detail.x - data.left, y: e.detail.y - data.top }, currentIndex: currentIndex, legendIndex: legendIndex, id: cid, opts: cfu.instance[cid].opts}});
}
})
.exec();
} else {
if(move){
if (this.data.tooltipShow === true) {
this._showTooltip(e);
}
}else{
e.changedTouches=[];
e.changedTouches.unshift({ x: e.detail.x - e.currentTarget.offsetLeft, y: e.detail.y - e.currentTarget.offsetTop });
currentIndex = cfu.instance[cid].getCurrentDataIndex(e);
legendIndex = cfu.instance[cid].getLegendDataIndex(e);
if(this.data.tapLegend === true){
cfu.instance[cid].touchLegend(e);
}
if (this.data.tooltipShow === true) {
this._showTooltip(e);
}
this.emitMsg({name: 'getIndex', params: {type:"getIndex", event:{ x: e.detail.x, y: e.detail.y - e.currentTarget.offsetTop }, currentIndex: currentIndex, legendIndex: legendIndex, id: cid, opts: cfu.instance[cid].opts}});
}
}
},
_touchStart(e) {
let cid = this.data.cid
lastMoveTime=Date.now();
if(cfu.option[cid].enableScroll === true && e.touches.length == 1){
cfu.instance[cid].scrollStart(e);
}
this.emitMsg({name:'getTouchStart', params:{type:"touchStart", event:e.changedTouches, id:cid}});
},
_touchMove(e) {
let cid = this.data.cid
let currMoveTime = Date.now();
let duration = currMoveTime - lastMoveTime;
let touchMoveLimit = cfu.option[cid].touchMoveLimit || 24;
if (duration < Math.floor(1000 / touchMoveLimit)) return;//每秒60帧
lastMoveTime = currMoveTime;
if(cfu.option[cid].enableScroll === true && e.changedTouches.length == 1){
cfu.instance[cid].scroll(e);
}
if(this.data.ontap === true && cfu.option[cid].enableScroll === false && this.data.onmovetip === true){
this._tap(e,true)
}
if(this.data.ontouch === true && cfu.option[cid].enableScroll === true && this.data.onzoom === true && e.changedTouches.length == 2){
cfu.instance[cid].dobuleZoom(e);
}
this.emitMsg({name: 'getTouchMove', params: {type:"touchMove", event:e.changedTouches, id: cid}});
},
_touchEnd(e) {
let cid = this.data.cid
if(cfu.option[cid].enableScroll === true && e.touches.length == 0){
cfu.instance[cid].scrollEnd(e);
}
this.emitMsg({name:'getTouchEnd', params:{type:"touchEnd", event:e.changedTouches, id:cid}});
if(this.data.ontap === true && cfu.option[cid].enableScroll === false && this.data.onmovetip === true){
this._tap(e,true)
}
},
_error(e) {
this.data.mixinDatacomErrorMessage = e.detail.errMsg;
},
emitMsg(msg) {
this.triggerEvent(msg.name, msg.params);
},
toJSON(){
return this
}
}
})