744 lines
25 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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
}
}
})