| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492 |
- import { mix, isFunction, createEvent, each, isObjectValueEqual, deepMix, addEventListener, removeEventListener } from '../util/common';
- import { Group } from '../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.maxX))
- &&
- (Math.max(label1BBox.minY, label2BBox.minY) <= Math.min(label1BBox.maxY, label2BBox.maxY))
- );
- }
- class controller {
- constructor(cfg) {
- 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 (isFunction(label1)) {
- textGroup.addShape('Text', {
- attrs: mix({
- textBaseline: 'bottom'
- }, textAttrs, label1(_origin, color)),
- data: _origin, // 存储原始数据
- offsetY: label1OffsetY
- });
- }
- if (isFunction(label2)) {
- textGroup.addShape('Text', {
- attrs: 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';
- addEventListener(this.canvasDom, triggerOn, this._handleEvent);
- }
- unBindEvents() {
- const pieLabelCfg = this.pieLabelCfg;
- const triggerOn = pieLabelCfg.triggerOn || 'touchstart';
- removeEventListener(this.canvasDom, triggerOn, this._handleEvent);
- }
- 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: mix({
- points,
- lineWidth: 1,
- stroke: fill
- }, lineStyle)
- });
- // 绘制锚点
- labelGroup.addShape('Circle', {
- attrs: 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 = 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');
- each(children, child => {
- if (child.get('isShape') && (child.get('className') === geom.get('type'))) { // get geometry's shape
- const shapeData = child.get('origin')._origin;
- if (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: mix({
- x,
- y,
- r: r + activeStyle.offset + activeStyle.appendRadius,
- r0: r + activeStyle.offset,
- fill,
- startAngle,
- endAngle
- }, activeStyle)
- });
- this.halo = halo;
- chart.get('canvas').draw();
- }
- }
- function 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 = deepMix({}, DEFAULT_CFG, cfg);
- pieLabelController.pieLabelCfg = cfg;
- return this;
- };
- }
- function afterGeomDraw(chart) {
- const controller = chart.get('pieLabelController');
- if (controller.pieLabelCfg) { // 用户配置了饼图文本
- controller.renderLabels();
- controller.bindEvents(); // 绑定事件
- }
- }
- function clearInner(chart) {
- const controller = chart.get('pieLabelController');
- if (controller.pieLabelCfg) { // 用户配置了饼图文本
- controller.clear();
- }
- }
- export {
- init,
- afterGeomDraw,
- clearInner
- };
- export default {
- init,
- afterGeomDraw,
- clearInner
- };
|