UNPKG

light-chart

Version:

Charts for mobile visualization.

482 lines (433 loc) 14 kB
const Util = require('../util/common'); const { Group } = require('../graphic/'); const DEFAULT_CFG = { anchorOffset: 5, // 锚点的偏移量 inflectionOffset: 15, // 拐点的偏移量 sidePadding: 20, // 文本距离画布四边的距离 lineHeight: 32, // 文本的行高 adjustOffset: 15, // 发生调整时的偏移量 skipOverlapLabels: false, // 是否不展示重叠的文本 triggerOn: 'touchstart', // 点击行为触发的时间类型 activeShape: false, // 当有图形被选中的时候,是否激活图形 activeStyle: { offset: 1, appendRadius: 8, fillOpacity: 0.5 }, label1OffsetY: -1, label2OffsetY: 1 }; function getEndPoint(center, angle, r) { return { x: center.x + r * Math.cos(angle), y: center.y + r * Math.sin(angle) }; } // 计算中间角度 function getMiddleAngle(startAngle, endAngle) { if (endAngle < startAngle) { endAngle += Math.PI * 2; } return (endAngle + startAngle) / 2; } // 判断两个矩形是否相交 function isOverlap(label1, label2) { const label1BBox = label1.getBBox(); const label2BBox = label2.getBBox(); return ( (Math.max(label1BBox.minX, label2BBox.minX) <= Math.min(label1BBox.maxX, label2BBox.minX)) && (Math.max(label1BBox.minY, label2BBox.minY) <= Math.min(label1BBox.maxY, label2BBox.maxY)) ); } class controller { constructor(cfg) { Util.mix(this, cfg); const chart = this.chart; this.canvasDom = chart.get('canvas').get('el'); } renderLabels() { const self = this; const { chart, pieLabelCfg, labelGroup } = self; const halves = [ [], // left [] // right ]; // 存储左右 labels const geom = chart.get('geoms')[0]; const shapes = geom.get('container').get('children'); const { anchorOffset, inflectionOffset, label1, label2, lineHeight, skipOverlapLabels, label1OffsetY, label2OffsetY } = pieLabelCfg; const coord = chart.get('coord'); const { center, circleRadius: radius } = coord; shapes.forEach(shape => { const { startAngle, endAngle } = shape._attrs.attrs; const middleAngle = getMiddleAngle(startAngle, endAngle); const anchorPoint = getEndPoint(center, middleAngle, radius + anchorOffset); const inflectionPoint = getEndPoint(center, middleAngle, radius + inflectionOffset); const origin = shape.get('origin'); const { _origin, color } = origin; const label = { _anchor: anchorPoint, _inflection: inflectionPoint, _data: _origin, x: inflectionPoint.x, y: inflectionPoint.y, r: radius + inflectionOffset, fill: color }; const textGroup = new Group({ context: chart.get('canvas').get('context'), // 兼容 node、小程序环境 data: _origin // 存储原始数据 }); const textAttrs = { x: 0, y: 0, fontSize: 12, lineHeight: 12, fill: '#808080' }; if (Util.isFunction(label1)) { textGroup.addShape('Text', { attrs: Util.mix({ textBaseline: 'bottom' }, textAttrs, label1(_origin, color)), data: _origin, // 存储原始数据 offsetY: label1OffsetY }); } if (Util.isFunction(label2)) { textGroup.addShape('Text', { attrs: Util.mix({ textBaseline: 'top' }, textAttrs, label2(_origin, color)), data: _origin, // 存储原始数据 offsetY: label2OffsetY }); } label.textGroup = textGroup; // 判断文本的方向 if (anchorPoint.x < center.x) { label._side = 'left'; halves[0].push(label); } else { label._side = 'right'; halves[1].push(label); } }); let drawnLabels = []; if (skipOverlapLabels) { let lastLabel; // 存储上一个 label 对象,用于检测文本是否重叠 const labels = halves[1].concat(halves[0]); // 顺时针 for (let i = 0, len = labels.length; i < len; i++) { const label = labels[i]; const textGroup = self._drawLabel(label); if (lastLabel) { if (isOverlap(textGroup, lastLabel)) { // 重叠了就不绘制 continue; } } labelGroup.add(textGroup); self._drawLabelLine(label); lastLabel = textGroup; drawnLabels.push(textGroup); } } else { const height = chart.get('height'); const maxCountForOneSide = parseInt(height / lineHeight, 10); halves.forEach(half => { if (half.length > maxCountForOneSide) { half.splice(maxCountForOneSide, half.length - maxCountForOneSide); } half.sort((a, b) => { return a.y - b.y; }); const labels = self._antiCollision(half); drawnLabels = drawnLabels.concat(labels); }); } this.drawnLabels = drawnLabels; } bindEvents() { const pieLabelCfg = this.pieLabelCfg; const triggerOn = pieLabelCfg.triggerOn || 'touchstart'; const method = Util.wrapBehavior(this, '_handleEvent'); Util.addEventListener(this.canvasDom, triggerOn, method); } unBindEvents() { const pieLabelCfg = this.pieLabelCfg; const triggerOn = pieLabelCfg.triggerOn || 'touchstart'; const method = Util.getWrapBehavior(this, '_handleEvent'); Util.removeEventListener(this.canvasDom, triggerOn, method); } clear() { this.labelGroup && this.labelGroup.clear(); this.halo && this.halo.remove(true); this.lastSelectedData = null; this.drawnLabels = []; this.unBindEvents(); } _drawLabel(label) { const { pieLabelCfg, chart } = this; const canvasWidth = chart.get('width'); const { sidePadding } = pieLabelCfg; const { y, textGroup } = label; const children = textGroup.get('children'); const textAttrs = { textAlign: label._side === 'left' ? 'left' : 'right', x: label._side === 'left' ? sidePadding : canvasWidth - sidePadding }; children.forEach(child => { child.attr(textAttrs); child.attr('y', y + child.get('offsetY')); }); return textGroup; } _drawLabelLine(label, maxLabelWidth) { const { chart, pieLabelCfg, labelGroup } = this; const canvasWidth = chart.get('width'); const { sidePadding, adjustOffset, lineStyle, anchorStyle, skipOverlapLabels } = pieLabelCfg; const { _anchor, _inflection, fill, y } = label; const lastPoint = { x: label._side === 'left' ? sidePadding : canvasWidth - sidePadding, y }; let points = [ _anchor, _inflection, lastPoint ]; if (!skipOverlapLabels && _inflection.y !== y) { // 展示全部文本文本位置做过调整 if (_inflection.y < y) { // 文本被调整下去了,则添加拐点连接线 const point1 = _inflection; const point2 = { x: label._side === 'left' ? lastPoint.x + maxLabelWidth + adjustOffset : lastPoint.x - maxLabelWidth - adjustOffset, y: _inflection.y }; const point3 = { x: label._side === 'left' ? lastPoint.x + maxLabelWidth : lastPoint.x - maxLabelWidth, y: lastPoint.y }; points = [ _anchor, point1, point2, point3, lastPoint ]; if ((label._side === 'right' && point2.x < point1.x) || (label._side === 'left' && point2.x > point1.x)) { points = [ _anchor, point3, lastPoint ]; } } else { points = [ _anchor, { x: _inflection.x, y }, lastPoint ]; } } labelGroup.addShape('Polyline', { attrs: Util.mix({ points, lineWidth: 1, stroke: fill }, lineStyle) }); // 绘制锚点 labelGroup.addShape('Circle', { attrs: Util.mix({ x: _anchor.x, y: _anchor.y, r: 2, fill }, anchorStyle) }); } _antiCollision(half) { const self = this; const { chart, pieLabelCfg } = self; const coord = chart.get('coord'); const canvasHeight = chart.get('height'); const { center, circleRadius: r } = coord; const { inflectionOffset, lineHeight } = pieLabelCfg; const startY = center.y - r - inflectionOffset - lineHeight; let overlapping = true; let totalH = canvasHeight; let i; let maxY = 0; let minY = Number.MIN_VALUE; let maxLabelWidth = 0; const boxes = half.map(function(label) { const labelY = label.y; if (labelY > maxY) { maxY = labelY; } if (labelY < minY) { minY = labelY; } const textGroup = label.textGroup; const labelWidth = textGroup.getBBox().width; if (labelWidth >= maxLabelWidth) { maxLabelWidth = labelWidth; } return { size: lineHeight, targets: [ labelY - startY ] }; }); if ((maxY - startY) > totalH) { totalH = maxY - startY; } const iteratorBoxed = function(boxes) { boxes.forEach(box => { const target = (Math.min.apply(minY, box.targets) + Math.max.apply(minY, box.targets)) / 2; box.pos = Math.min(Math.max(minY, target - box.size / 2), totalH - box.size); }); }; while (overlapping) { iteratorBoxed(boxes); // detect overlapping and join boxes overlapping = false; i = boxes.length; while (i--) { if (i > 0) { const previousBox = boxes[i - 1]; const box = boxes[i]; if (previousBox.pos + previousBox.size > box.pos) { // overlapping previousBox.size += box.size; previousBox.targets = previousBox.targets.concat(box.targets); // overflow, shift up if (previousBox.pos + previousBox.size > totalH) { previousBox.pos = totalH - previousBox.size; } boxes.splice(i, 1); // removing box overlapping = true; } } } } i = 0; boxes.forEach(function(b) { let posInCompositeBox = startY; // middle of the label b.targets.forEach(function() { half[i].y = b.pos + posInCompositeBox + lineHeight / 2; posInCompositeBox += lineHeight; i++; }); }); const drawnLabels = []; half.forEach(function(label) { const textGroup = self._drawLabel(label); const labelGroup = self.labelGroup; labelGroup.add(textGroup); self._drawLabelLine(label, maxLabelWidth); drawnLabels.push(textGroup); }); return drawnLabels; } _handleEvent(ev) { const self = this; const { chart, drawnLabels, pieLabelCfg } = self; const { onClick, activeShape } = pieLabelCfg; const canvasEvent = Util.createEvent(ev, chart); const { x, y } = canvasEvent; // 查找被点击的 label let clickedShape; for (let i = 0, len = drawnLabels.length; i < len; i++) { const shape = drawnLabels[i]; const bbox = shape.getBBox(); // 通过最小包围盒来判断击中情况 if (x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY) { clickedShape = shape; break; } } const pieData = chart.getSnapRecords({ x, y }); if (clickedShape) { canvasEvent.data = clickedShape.get('data'); } else if (pieData.length) { // 击中饼图扇形区域 canvasEvent.data = pieData[0]._origin; } onClick && onClick(canvasEvent); canvasEvent.data && activeShape && this._activeShape(canvasEvent.data); } _getSelectedShapeByData(data) { let selectedShape = null; const chart = this.chart; const geom = chart.get('geoms')[0]; const container = geom.get('container'); const children = container.get('children'); Util.each(children, child => { if (child.get('isShape') && (child.get('className') === geom.get('type'))) { // get geometry's shape const shapeData = child.get('origin')._origin; if (Util.isObjectValueEqual(shapeData, data)) { selectedShape = child; return false; } } }); return selectedShape; } _activeShape(data) { const { chart, lastSelectedData, pieLabelCfg } = this; if (data === lastSelectedData) { return; } this.lastSelectedData = data; const activeStyle = pieLabelCfg.activeStyle; const selectedShape = this._getSelectedShapeByData(data); const { x, y, startAngle, endAngle, r, fill } = selectedShape._attrs.attrs; const frontPlot = chart.get('frontPlot'); this.halo && this.halo.remove(true); const halo = frontPlot.addShape('sector', { attrs: Util.mix({ x, y, r: r + activeStyle.offset + activeStyle.appendRadius, r0: r + activeStyle.offset, fill, startAngle, endAngle }, activeStyle) }); this.halo = halo; chart.get('canvas').draw(); } } module.exports = { init(chart) { const frontPlot = chart.get('frontPlot'); const labelGroup = frontPlot.addGroup({ className: 'pie-label', zIndex: 0 }); const pieLabelController = new controller({ chart, labelGroup }); chart.set('pieLabelController', pieLabelController); chart.pieLabel = function(cfg) { cfg = Util.deepMix({}, DEFAULT_CFG, cfg); pieLabelController.pieLabelCfg = cfg; return this; }; }, afterGeomDraw(chart) { const controller = chart.get('pieLabelController'); if (controller.pieLabelCfg) { // 用户配置了饼图文本 controller.renderLabels(); controller.bindEvents(); // 绑定事件 } }, clearInner(chart) { const controller = chart.get('pieLabelController'); if (controller.pieLabelCfg) { // 用户配置了饼图文本 controller.clear(); } } };