UNPKG

chartx3d

Version:

Data Visualization Chart Library

561 lines (467 loc) 20 kB
import { global } from 'mmvis'; import { Component, _ } from '../../Component'; import { Vector3, MeshBasicMaterial, MeshLambertMaterial, FrontSide, DoubleSide, MeshPhongMaterial, Color, Box3, Math as _Math, CatmullRomCurve3 } from 'mmgl/src/index'; let renderOrder = 0; class Pie extends Component { constructor(chart3d, opt) { super(chart3d.currCoord); this.name = 'Pie'; this.type = "pie3d"; this._type = "pie"; this.field = null; this.sort = null; //默认不排序,可以配置为asc,desc //groupField主要是给legend用的, 所有在legend中需要显示的分组数据,都用groupField //其他图也都统一, 不要改 this.groupField = null; this.maxHeight = 0; this.heightSort = null; //默认不排序,可以配置为asc,desc this.offsetRadius = 5; this.origin = { x: 0, y: 0, z: 0 }; this.node = { shapeType: "sector", radius: null, //每个扇形单元的半径,也可以配置一个字段,就成了丁格尔玫瑰图 innerRadius: 0, //扇形的内圆半径 outRadius: null,//最大外围半径 minRadius: 10,//outRadius - innerRadius , 也就是radius的最小值 moveDis: 30, //要预留moveDis位置来hover sector 的时候外扩 height: 50, fillStyle: null, //this.root.getTheme(), focus: { enabled: true, }, select: { enabled: false, radius: 5, alpha: 0.7 } }; this.label = { field: null, //默认获取field的值,但是可以单独设置 enabled: false, format: null, offset: 10 }; this.startAngle = -90; this.allAngles = 360; this.isInit = true; this.init(opt); } init(opt) { _.extend(true, this, opt); //计算一下图形的半径 let attr = this._coordSystem.dataAttribute; //根据当前的boundbox,如果用户没有制定半径这里需要计算出来 this._computerProps(); attr.setOption(Object.assign({}, this)); //已经在坐标系中传入构造函数中这里可以不传 attr.setDataFrame(); this.textGroup = this._root.app.addGroup({ name: 'pie_texts_gruop' }); this._root.labelGroup.add(this.textGroup); } _computerProps() { let { x, y, z } = this._coordSystem.getSize(); //根据配置情况重新修正 outRadius ,innerRadius ------------ if (!this.node.outRadius) { var outRadius = Math.min(x, z) / 2; if (this.label.enabled) { //要预留moveDis位置来hover sector 的时候外扩 outRadius -= this.node.moveDis; }; this.node.outRadius = outRadius; }; if (this.node.radius !== null && _.isNumber(this.node.radius)) { //如果用户有直接配置 radius,那么radius优先,用来计算 this.node.radius = Math.max(this.node.radius, this.node.minRadius); //this.node.outRadius = this.node.innerRadius + this.node.radius; this.node.innerRadius = this.node.outRadius - this.node.radius; }; //要保证sec具有一个最小的radius if (this.node.outRadius - this.node.innerRadius < this.node.minRadius) { this.node.innerRadius = this.node.outRadius - this.node.minRadius; }; if (this.node.innerRadius < 0) { this.node.innerRadius = 0; }; // end -------------------------------------------------- if (!this.maxHeight) { this.maxHeight = y * 0.5; } } draw() { let me = this; let app = this._root.app; let attr = this._coordSystem.dataAttribute; let heights = []; attr.calculateProps(); this.data = attr.getLayoutData(); this.dispose(); this.dispose(this.textGroup); this.data.forEach((item, i) => { if (!item.enabled) return; //文本格式化 item.labelText = this._getLabelText(item); let material = new MeshPhongMaterial({ color: item.color, transparent: true, opacity: 1.0, depthTest: true, depthWrite: true, side: DoubleSide, polygonOffset: true, polygonOffsetFactor: Math.sin(_Math.degToRad(item.midAngle)), polygonOffsetUnits: 1.5 }) heights.push(item.height); let sector = app.create3DPie(item.height, item.outRadius, item.innerRadius, item.startAngle, item.endAngle, material); sector.userData.midAngle = item.midAngle; //sector.renderOrder = renderOrder++; sector.name = Pie._pie_prefix + item.iNode; sector.userData.info = item; //如果设置了高度取相应的数据,需要给定一些间距,防止z-fighting,尤其室圆心位置 if (item.heightField && item.innerRadius == 0) { let offset = this.data.length * this.offsetRadius / (2 * Math.PI); let x = offset * Math.cos(_Math.degToRad(item.midAngle)); let z = offset * Math.sin(_Math.degToRad(item.midAngle)) sector.position.set(x, 0, z); } this.group.add(sector); }) this.adjustPosition(heights); if (this.label.enabled) { this._startWidgetLabel(); }; this.bindEvent(); } bindEvent() { let me = this; this.__mouseover = function (e) { //上下文中的this 是bar 对象 this.userData.color = this.material.color.clone(); //高亮 let tempColor = {}; this.material.color.getHSL(tempColor); this.material.setValues({ color: new Color().setHSL(tempColor.h, tempColor.s, tempColor.l + 0.05) }); let _data = this.userData.info; me._root.fire({ type: 'tipShow', event: e.event, data: { nodes: [_data] } }); me.fire({ type: 'sectorover', data: _data }); }; this.__mouseout = function (e) { this.material.setValues({ color: this.userData.color }); let _data = this.userData.info; me._root.fire({ type: 'tipHide', event: e.event, data: { nodes: [_data] } }); me.fire({ type: 'sectorout', data: _data }); }; this.__mousemove = function (e) { let _data = this.userData.info; me._root.fire({ type: 'tipMove', event: e.event, data: { nodes: [_data] } }); me.fire({ type: 'sectormove', data: _data }); }; this.__click = function (e) { let _data = this.userData.info; if (!this.userData.isChecked) { this.userData.isChecked = true; //移动位置 let moveDis = _data.moveDis; let pos = this.position.clone(); this.userData.orgPosition = pos; let x = moveDis * Math.cos(_Math.degToRad(this.userData.midAngle)); let z = moveDis * Math.sin(_Math.degToRad(this.userData.midAngle)); this.position.set(x, pos.y, z) } else { this.userData.isChecked = false; if (this.userData.orgPosition) { this.position.copy(this.userData.orgPosition); } } me.fire({ type: 'sectorclick', data: _data }); } this.group.traverse(sector => { if (sector.name && sector.name.includes(Pie._pie_prefix)) { sector.on('mouseover', this.__mouseover); sector.on('mouseout', this.__mouseout); sector.on('mousemove', this.__mousemove); sector.on('click', this.__click); } }); } _getLabelText(itemData) { var str; if (this.label.enabled) { if (this.label.format) { if (_.isFunction(this.label.format)) { str = this.label.format(itemData.label, itemData); } } else { var _field = this.label.field || this.groupField if (_field) { str = itemData.rowData[_field] + ":" + itemData.percentage + "%" } else { str = itemData.percentage + "%" } } } return str; } _startWidgetLabel() { var me = this; let attr = this._coordSystem.dataAttribute; var data = attr.getLayoutData(); var rMinPercentage = 0, lMinPercentage = 0, rMinY = 0, lMinY = 0; var quadrantsOrder = []; var quadrantInfo = [ { indexs: [], count: 0 }, { indexs: [], count: 0 }, { indexs: [], count: 0 }, { indexs: [], count: 0 } ]; //默认从top开始画 var widgetInfo = { right: { startQuadrant: 4, endQuadrant: 1, clockwise: true, indexs: [] }, left: { startQuadrant: 3, endQuadrant: 2, clockwise: false, indexs: [] } }; for (var i = 0; i < data.length; i++) { var cur = data[i].quadrant; quadrantInfo[cur - 1].indexs.push(i); quadrantInfo[cur - 1].count++; } //1,3象限的绘制顺序需要反转 if (quadrantInfo[0].count > 1) quadrantInfo[0].indexs.reverse(); if (quadrantInfo[2].count > 1) quadrantInfo[2].indexs.reverse(); if (quadrantInfo[0].count > quadrantInfo[3].count) { widgetInfo.right.startQuadrant = 1; widgetInfo.right.endQuadrant = 4; widgetInfo.right.clockwise = false; } if (quadrantInfo[1].count > quadrantInfo[2].count) { widgetInfo.left.startQuadrant = 2; widgetInfo.left.endQuadrant = 3; widgetInfo.left.clockwise = true; } widgetInfo.right.indexs = quadrantInfo[widgetInfo.right.startQuadrant - 1].indexs.concat(quadrantInfo[widgetInfo.right.endQuadrant - 1].indexs); widgetInfo.left.indexs = quadrantInfo[widgetInfo.left.startQuadrant - 1].indexs.concat(quadrantInfo[widgetInfo.left.endQuadrant - 1].indexs); var overflowIndexs, sortedIndexs; if (widgetInfo.right.indexs.length > me.textMaxCount) { sortedIndexs = widgetInfo.right.indexs.slice(0); sortedIndexs.sort(function (a, b) { return data[b].y - data[a].y; }); overflowIndexs = sortedIndexs.slice(me.textMaxCount); rMinPercentage = data[overflowIndexs[0]].percentage; rMinY = data[overflowIndexs[0]].y; } if (widgetInfo.left.indexs.length > me.textMaxCount) { sortedIndexs = widgetInfo.left.indexs.slice(0); sortedIndexs.sort(function (a, b) { return data[b].y - data[a].y; }); overflowIndexs = sortedIndexs.slice(me.textMaxCount); lMinPercentage = data[overflowIndexs[0]].percentage; lMinY = data[overflowIndexs[0]].y; } quadrantsOrder.push(widgetInfo.right.startQuadrant); quadrantsOrder.push(widgetInfo.right.endQuadrant); quadrantsOrder.push(widgetInfo.left.startQuadrant); quadrantsOrder.push(widgetInfo.left.endQuadrant); var ySpaceInfo = {} for (var i = 0; i < quadrantsOrder.length; i++) { var isEnd = i == 1 || i == 3; me._widgetLabel(quadrantsOrder[i], quadrantInfo[quadrantsOrder[i] - 1].indexs, lMinY, rMinY, isEnd, ySpaceInfo) } } _widgetLabel(quadrant, indexs, lmin, rmin, isEnd, ySpaceInfo) { var me = this; var count = 0; let attr = this._coordSystem.dataAttribute; var data = attr.getLayoutData(); let labels = []; var minTxtDis = 15; var textOffsetX = 5; let app = this._root.app; var currentIndex; var preY, currentY, adjustX, txtDis, bwidth, bheight, bx, by; var yBound, remainingNum, remainingY; var clockwise = quadrant == 2 || quadrant == 4; var isleft = quadrant == 2 || quadrant == 3; var isup = quadrant == 3 || quadrant == 4; var minY = isleft ? lmin : rmin; //text的绘制顺序做修正,text的Y值在饼图上半部分(isup)时,Y值越小的先画,反之Y值在饼图下部分时,Y值越大的先画. if (indexs.length > 0) { indexs.sort(function (a, b) { return isup ? data[a].edgey - data[b].edgey : data[b].edgey - data[a].edgey; }) } for (var i = 0; i < indexs.length; i++) { currentIndex = indexs[i]; var itemData = data[currentIndex]; var outCircleRadius = itemData.outRadius + itemData.moveDis; //若Y值小于最小值,不画text if (!itemData.enabled || itemData.y < minY || count >= me.textMaxCount) continue count++; currentY = itemData.edgey; adjustX = Math.abs(itemData.edgex); txtDis = currentY - preY; if (i != 0 && ((Math.abs(txtDis) < minTxtDis) || (isup && txtDis < 0) || (!isup && txtDis > 0))) { currentY = isup ? preY + minTxtDis : preY - minTxtDis; if (outCircleRadius - Math.abs(currentY) > 0) { adjustX = Math.sqrt(Math.pow(outCircleRadius, 2) - Math.pow(currentY, 2)); } if ((isleft && (-adjustX > itemData.edgex)) || (!isleft && (adjustX < itemData.edgex))) { adjustX = Math.abs(itemData.edgex); } } if (isEnd) { yBound = isleft ? ySpaceInfo.left : ySpaceInfo.right; remainingNum = indexs.length - i; remainingY = isup ? yBound - remainingNum * minTxtDis : yBound + remainingNum * minTxtDis; if ((isup && currentY > remainingY) || !isup && currentY < remainingY) { currentY = remainingY; } } preY = currentY; if (!isEnd) { if (isleft) { ySpaceInfo.left = preY; } else { ySpaceInfo.right = preY; } }; var currentX = (isleft ? -adjustX - textOffsetX : adjustX + textOffsetX); var globalX = currentX + me.origin.x; var globalY = currentY + me.origin.y; // if (globalX > me._graphs.root.width || globalY < 0 || globalY > me._graphs.root.height) { // return; // }; // var pathStr = "M" + itemData.centerx + "," + itemData.centery; // pathStr += "Q" + itemData.outx + "," + itemData.outy + "," + currentX + "," + currentY; let DIVISONS = 50; let linePoints = [ new Vector3(itemData.centerx, 0, itemData.centery), new Vector3(itemData.outx, 0, itemData.outy), new Vector3(currentX, 0, currentY), ] let curve = new CatmullRomCurve3(linePoints); let points = curve.getSpacedPoints(DIVISONS); let line = app.createBrokenLine(points, 2, itemData.color, true); this.group.add(line); let label = app.creatSpriteText(itemData.labelText, { fontSize: 14, fillStyle: itemData.color }); label[0].userData.position = new Vector3(currentX, 0, currentY); label[0].userData.dir = new Vector3(itemData.outx, 0, itemData.outy).sub(new Vector3(itemData.centerx, 0, itemData.centery)) label[0].userData.dir.normalize(); labels.push(label[0]); } labels.forEach((label, index) => { label.userData.position = label.userData.position label.matrixWorldNeedsUpdate = false; label.onBeforeRender = function (render, scene, camera) { // //更新坐标后的位置 let cross = me._coordSystem.getQuadrantByDir(this.userData.dir.clone()); let pos = me._coordSystem.positionToScreen(this.userData.position.clone()); // //屏幕的位置 let textSize = this.userData.size; let halfwidth = textSize[0] * 0.5; let halfHeight = textSize[1] * 0.5 // let camearDir = new Vector3(); // camera.getWorldDirection(camearDir); // let isSameDir = zDir.dot(camearDir); //right if (cross.y > 0) { // let flag = isSameDir < 0 ? 1 : -1; pos.setX(pos.x - halfwidth - me.label.offset); // this.position.copy(pos); } //left if (cross.y < 0) { // let flag = isSameDir < 0 ? -1 : 1; pos.setX(pos.x + halfwidth + me.label.offset); // this.position.copy(pos); } // if (me.verticalAlign == 'top') { // pos.setY(pos.y - halfHeight); // label.position.copy(pos); // } // if (me.verticalAlign == 'bottom') { // pos.setY(pos.y + halfHeight); // label.position.copy(pos); // } this.position.copy(pos); this.updateMatrixWorld(true); } this.textGroup.add(label); }); } adjustPosition(heights) { //判断是否需要调整 let isAdjust = _.uniq(heights).length > 1; if (!isAdjust) return; let maxHeight = _.max(heights); this.group.traverse(obj => { if (obj.isMesh && obj.geometry && (obj.geometry.type === "DoughnutBufferGeometry" || obj.geometry.type === "DoughnutGeometry")) { let offset = (maxHeight - obj.geometry.parameters.height) * 0.5; obj.position.setY(- offset); } }) } dispose(group) { group = group || this.group; group.traverse((sector) => { if (sector.name && sector.name.includes(Pie._pie_prefix)) { sector.off('mouseover', this.__mouseover); sector.off('mouseout', this.__mouseout); sector.off('mousemove', this.__mousemove); sector.off('click', this.__click); } }) super.dispose(group); } resetData() { this.dispose(); let attr = this._coordSystem.dataAttribute; //根据当前的boundbox,如果用户没有制定半径这里需要计算出来 this._computerProps(); attr.setOption(Object.assign({}, this)); //已经在坐标系中传入构造函数中这里可以不传 attr.setDataFrame(); this.draw(); } } Pie._pie_prefix = "pie_one_"; global.registerComponent(Pie, 'graphs', 'pie', 3); export default Pie;