pie-label.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582
  1. function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
  2. import { mix, isFunction, createEvent, each, isObjectValueEqual, deepMix, addEventListener, removeEventListener } from '../util/common';
  3. import { Group } from '../graphic/';
  4. var DEFAULT_CFG = {
  5. anchorOffset: 5,
  6. // 锚点的偏移量
  7. inflectionOffset: 15,
  8. // 拐点的偏移量
  9. sidePadding: 20,
  10. // 文本距离画布四边的距离
  11. lineHeight: 32,
  12. // 文本的行高
  13. adjustOffset: 15,
  14. // 发生调整时的偏移量
  15. skipOverlapLabels: false,
  16. // 是否不展示重叠的文本
  17. triggerOn: 'touchstart',
  18. // 点击行为触发的时间类型
  19. activeShape: false,
  20. // 当有图形被选中的时候,是否激活图形
  21. activeStyle: {
  22. offset: 1,
  23. appendRadius: 8,
  24. fillOpacity: 0.5
  25. },
  26. label1OffsetY: -1,
  27. label2OffsetY: 1
  28. };
  29. function getEndPoint(center, angle, r) {
  30. return {
  31. x: center.x + r * Math.cos(angle),
  32. y: center.y + r * Math.sin(angle)
  33. };
  34. } // 计算中间角度
  35. function getMiddleAngle(startAngle, endAngle) {
  36. if (endAngle < startAngle) {
  37. endAngle += Math.PI * 2;
  38. }
  39. return (endAngle + startAngle) / 2;
  40. } // 判断两个矩形是否相交
  41. function isOverlap(label1, label2) {
  42. var label1BBox = label1.getBBox();
  43. var label2BBox = label2.getBBox();
  44. 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);
  45. }
  46. class controller {
  47. constructor(cfg) {
  48. var _this = this;
  49. _defineProperty(this, "_handleEvent", function (ev) {
  50. var self = _this;
  51. var {
  52. chart,
  53. drawnLabels,
  54. pieLabelCfg
  55. } = self;
  56. var {
  57. onClick,
  58. activeShape
  59. } = pieLabelCfg;
  60. var canvasEvent = createEvent(ev, chart);
  61. var {
  62. x,
  63. y
  64. } = canvasEvent; // 查找被点击的 label
  65. var clickedShape;
  66. for (var i = 0, len = drawnLabels.length; i < len; i++) {
  67. var shape = drawnLabels[i];
  68. var bbox = shape.getBBox(); // 通过最小包围盒来判断击中情况
  69. if (x >= bbox.minX && x <= bbox.maxX && y >= bbox.minY && y <= bbox.maxY) {
  70. clickedShape = shape;
  71. break;
  72. }
  73. }
  74. var pieData = chart.getSnapRecords({
  75. x,
  76. y
  77. });
  78. if (clickedShape) {
  79. canvasEvent.data = clickedShape.get('data');
  80. } else if (pieData.length) {
  81. // 击中饼图扇形区域
  82. canvasEvent.data = pieData[0]._origin;
  83. }
  84. onClick && onClick(canvasEvent);
  85. canvasEvent.data && activeShape && _this._activeShape(canvasEvent.data);
  86. });
  87. mix(this, cfg);
  88. var _chart = this.chart;
  89. this.canvasDom = _chart.get('canvas').get('el');
  90. }
  91. renderLabels() {
  92. var self = this;
  93. var {
  94. chart,
  95. pieLabelCfg,
  96. labelGroup
  97. } = self;
  98. var halves = [[], // left
  99. [] // right
  100. ]; // 存储左右 labels
  101. var geom = chart.get('geoms')[0];
  102. var shapes = geom.get('container').get('children');
  103. var {
  104. anchorOffset,
  105. inflectionOffset,
  106. label1,
  107. label2,
  108. lineHeight,
  109. skipOverlapLabels,
  110. label1OffsetY,
  111. label2OffsetY
  112. } = pieLabelCfg;
  113. var coord = chart.get('coord');
  114. var {
  115. center,
  116. circleRadius: radius
  117. } = coord;
  118. shapes.forEach(function (shape) {
  119. var {
  120. startAngle,
  121. endAngle
  122. } = shape._attrs.attrs;
  123. var middleAngle = getMiddleAngle(startAngle, endAngle);
  124. var anchorPoint = getEndPoint(center, middleAngle, radius + anchorOffset);
  125. var inflectionPoint = getEndPoint(center, middleAngle, radius + inflectionOffset);
  126. var origin = shape.get('origin');
  127. var {
  128. _origin,
  129. color
  130. } = origin;
  131. var label = {
  132. _anchor: anchorPoint,
  133. _inflection: inflectionPoint,
  134. _data: _origin,
  135. x: inflectionPoint.x,
  136. y: inflectionPoint.y,
  137. r: radius + inflectionOffset,
  138. fill: color
  139. };
  140. var textGroup = new Group({
  141. context: chart.get('canvas').get('context'),
  142. // 兼容 node、小程序环境
  143. data: _origin // 存储原始数据
  144. });
  145. var textAttrs = {
  146. x: 0,
  147. y: 0,
  148. fontSize: 12,
  149. lineHeight: 12,
  150. fill: '#808080'
  151. };
  152. if (isFunction(label1)) {
  153. textGroup.addShape('Text', {
  154. attrs: mix({
  155. textBaseline: 'bottom'
  156. }, textAttrs, label1(_origin, color)),
  157. data: _origin,
  158. // 存储原始数据
  159. offsetY: label1OffsetY
  160. });
  161. }
  162. if (isFunction(label2)) {
  163. textGroup.addShape('Text', {
  164. attrs: mix({
  165. textBaseline: 'top'
  166. }, textAttrs, label2(_origin, color)),
  167. data: _origin,
  168. // 存储原始数据
  169. offsetY: label2OffsetY
  170. });
  171. }
  172. label.textGroup = textGroup; // 判断文本的方向
  173. if (anchorPoint.x < center.x) {
  174. label._side = 'left';
  175. halves[0].push(label);
  176. } else {
  177. label._side = 'right';
  178. halves[1].push(label);
  179. }
  180. });
  181. var drawnLabels = [];
  182. if (skipOverlapLabels) {
  183. var lastLabel; // 存储上一个 label 对象,用于检测文本是否重叠
  184. var labels = halves[1].concat(halves[0]); // 顺时针
  185. for (var i = 0, len = labels.length; i < len; i++) {
  186. var label = labels[i];
  187. var textGroup = self._drawLabel(label);
  188. if (lastLabel) {
  189. if (isOverlap(textGroup, lastLabel)) {
  190. // 重叠了就不绘制
  191. continue;
  192. }
  193. }
  194. labelGroup.add(textGroup);
  195. self._drawLabelLine(label);
  196. lastLabel = textGroup;
  197. drawnLabels.push(textGroup);
  198. }
  199. } else {
  200. var height = chart.get('height');
  201. var maxCountForOneSide = parseInt(height / lineHeight, 10);
  202. halves.forEach(function (half) {
  203. if (half.length > maxCountForOneSide) {
  204. half.splice(maxCountForOneSide, half.length - maxCountForOneSide);
  205. }
  206. half.sort(function (a, b) {
  207. return a.y - b.y;
  208. });
  209. var labels = self._antiCollision(half);
  210. drawnLabels = drawnLabels.concat(labels);
  211. });
  212. }
  213. this.drawnLabels = drawnLabels;
  214. }
  215. bindEvents() {
  216. var pieLabelCfg = this.pieLabelCfg;
  217. var triggerOn = pieLabelCfg.triggerOn || 'touchstart';
  218. addEventListener(this.canvasDom, triggerOn, this._handleEvent);
  219. }
  220. unBindEvents() {
  221. var pieLabelCfg = this.pieLabelCfg;
  222. var triggerOn = pieLabelCfg.triggerOn || 'touchstart';
  223. removeEventListener(this.canvasDom, triggerOn, this._handleEvent);
  224. }
  225. clear() {
  226. this.labelGroup && this.labelGroup.clear();
  227. this.halo && this.halo.remove(true);
  228. this.lastSelectedData = null;
  229. this.drawnLabels = [];
  230. this.unBindEvents();
  231. }
  232. _drawLabel(label) {
  233. var {
  234. pieLabelCfg,
  235. chart
  236. } = this;
  237. var canvasWidth = chart.get('width');
  238. var {
  239. sidePadding
  240. } = pieLabelCfg;
  241. var {
  242. y,
  243. textGroup
  244. } = label;
  245. var children = textGroup.get('children');
  246. var textAttrs = {
  247. textAlign: label._side === 'left' ? 'left' : 'right',
  248. x: label._side === 'left' ? sidePadding : canvasWidth - sidePadding
  249. };
  250. children.forEach(function (child) {
  251. child.attr(textAttrs);
  252. child.attr('y', y + child.get('offsetY'));
  253. });
  254. return textGroup;
  255. }
  256. _drawLabelLine(label, maxLabelWidth) {
  257. var {
  258. chart,
  259. pieLabelCfg,
  260. labelGroup
  261. } = this;
  262. var canvasWidth = chart.get('width');
  263. var {
  264. sidePadding,
  265. adjustOffset,
  266. lineStyle,
  267. anchorStyle,
  268. skipOverlapLabels
  269. } = pieLabelCfg;
  270. var {
  271. _anchor,
  272. _inflection,
  273. fill,
  274. y
  275. } = label;
  276. var lastPoint = {
  277. x: label._side === 'left' ? sidePadding : canvasWidth - sidePadding,
  278. y
  279. };
  280. var points = [_anchor, _inflection, lastPoint];
  281. if (!skipOverlapLabels && _inflection.y !== y) {
  282. // 展示全部文本文本位置做过调整
  283. if (_inflection.y < y) {
  284. // 文本被调整下去了,则添加拐点连接线
  285. var point1 = _inflection;
  286. var point2 = {
  287. x: label._side === 'left' ? lastPoint.x + maxLabelWidth + adjustOffset : lastPoint.x - maxLabelWidth - adjustOffset,
  288. y: _inflection.y
  289. };
  290. var point3 = {
  291. x: label._side === 'left' ? lastPoint.x + maxLabelWidth : lastPoint.x - maxLabelWidth,
  292. y: lastPoint.y
  293. };
  294. points = [_anchor, point1, point2, point3, lastPoint];
  295. if (label._side === 'right' && point2.x < point1.x || label._side === 'left' && point2.x > point1.x) {
  296. points = [_anchor, point3, lastPoint];
  297. }
  298. } else {
  299. points = [_anchor, {
  300. x: _inflection.x,
  301. y
  302. }, lastPoint];
  303. }
  304. }
  305. labelGroup.addShape('Polyline', {
  306. attrs: mix({
  307. points,
  308. lineWidth: 1,
  309. stroke: fill
  310. }, lineStyle)
  311. }); // 绘制锚点
  312. labelGroup.addShape('Circle', {
  313. attrs: mix({
  314. x: _anchor.x,
  315. y: _anchor.y,
  316. r: 2,
  317. fill
  318. }, anchorStyle)
  319. });
  320. }
  321. _antiCollision(half) {
  322. var self = this;
  323. var {
  324. chart,
  325. pieLabelCfg
  326. } = self;
  327. var coord = chart.get('coord');
  328. var canvasHeight = chart.get('height');
  329. var {
  330. center,
  331. circleRadius: r
  332. } = coord;
  333. var {
  334. inflectionOffset,
  335. lineHeight
  336. } = pieLabelCfg;
  337. var startY = center.y - r - inflectionOffset - lineHeight;
  338. var overlapping = true;
  339. var totalH = canvasHeight;
  340. var i;
  341. var maxY = 0;
  342. var minY = Number.MIN_VALUE;
  343. var maxLabelWidth = 0;
  344. var boxes = half.map(function (label) {
  345. var labelY = label.y;
  346. if (labelY > maxY) {
  347. maxY = labelY;
  348. }
  349. if (labelY < minY) {
  350. minY = labelY;
  351. }
  352. var textGroup = label.textGroup;
  353. var labelWidth = textGroup.getBBox().width;
  354. if (labelWidth >= maxLabelWidth) {
  355. maxLabelWidth = labelWidth;
  356. }
  357. return {
  358. size: lineHeight,
  359. targets: [labelY - startY]
  360. };
  361. });
  362. if (maxY - startY > totalH) {
  363. totalH = maxY - startY;
  364. }
  365. var iteratorBoxed = function iteratorBoxed(boxes) {
  366. boxes.forEach(function (box) {
  367. var target = (Math.min.apply(minY, box.targets) + Math.max.apply(minY, box.targets)) / 2;
  368. box.pos = Math.min(Math.max(minY, target - box.size / 2), totalH - box.size);
  369. });
  370. };
  371. while (overlapping) {
  372. iteratorBoxed(boxes); // detect overlapping and join boxes
  373. overlapping = false;
  374. i = boxes.length;
  375. while (i--) {
  376. if (i > 0) {
  377. var previousBox = boxes[i - 1];
  378. var box = boxes[i];
  379. if (previousBox.pos + previousBox.size > box.pos) {
  380. // overlapping
  381. previousBox.size += box.size;
  382. previousBox.targets = previousBox.targets.concat(box.targets); // overflow, shift up
  383. if (previousBox.pos + previousBox.size > totalH) {
  384. previousBox.pos = totalH - previousBox.size;
  385. }
  386. boxes.splice(i, 1); // removing box
  387. overlapping = true;
  388. }
  389. }
  390. }
  391. }
  392. i = 0;
  393. boxes.forEach(function (b) {
  394. var posInCompositeBox = startY; // middle of the label
  395. b.targets.forEach(function () {
  396. half[i].y = b.pos + posInCompositeBox + lineHeight / 2;
  397. posInCompositeBox += lineHeight;
  398. i++;
  399. });
  400. });
  401. var drawnLabels = [];
  402. half.forEach(function (label) {
  403. var textGroup = self._drawLabel(label);
  404. var labelGroup = self.labelGroup;
  405. labelGroup.add(textGroup);
  406. self._drawLabelLine(label, maxLabelWidth);
  407. drawnLabels.push(textGroup);
  408. });
  409. return drawnLabels;
  410. }
  411. _getSelectedShapeByData(data) {
  412. var selectedShape = null;
  413. var chart = this.chart;
  414. var geom = chart.get('geoms')[0];
  415. var container = geom.get('container');
  416. var children = container.get('children');
  417. each(children, function (child) {
  418. if (child.get('isShape') && child.get('className') === geom.get('type')) {
  419. // get geometry's shape
  420. var shapeData = child.get('origin')._origin;
  421. if (isObjectValueEqual(shapeData, data)) {
  422. selectedShape = child;
  423. return false;
  424. }
  425. }
  426. });
  427. return selectedShape;
  428. }
  429. _activeShape(data) {
  430. var {
  431. chart,
  432. lastSelectedData,
  433. pieLabelCfg
  434. } = this;
  435. if (data === lastSelectedData) {
  436. return;
  437. }
  438. this.lastSelectedData = data;
  439. var activeStyle = pieLabelCfg.activeStyle;
  440. var selectedShape = this._getSelectedShapeByData(data);
  441. var {
  442. x,
  443. y,
  444. startAngle,
  445. endAngle,
  446. r,
  447. fill
  448. } = selectedShape._attrs.attrs;
  449. var frontPlot = chart.get('frontPlot');
  450. this.halo && this.halo.remove(true);
  451. var halo = frontPlot.addShape('sector', {
  452. attrs: mix({
  453. x,
  454. y,
  455. r: r + activeStyle.offset + activeStyle.appendRadius,
  456. r0: r + activeStyle.offset,
  457. fill,
  458. startAngle,
  459. endAngle
  460. }, activeStyle)
  461. });
  462. this.halo = halo;
  463. chart.get('canvas').draw();
  464. }
  465. }
  466. function init(chart) {
  467. var frontPlot = chart.get('frontPlot');
  468. var labelGroup = frontPlot.addGroup({
  469. className: 'pie-label',
  470. zIndex: 0
  471. });
  472. var pieLabelController = new controller({
  473. chart,
  474. labelGroup
  475. });
  476. chart.set('pieLabelController', pieLabelController);
  477. chart.pieLabel = function (cfg) {
  478. cfg = deepMix({}, DEFAULT_CFG, cfg);
  479. pieLabelController.pieLabelCfg = cfg;
  480. return this;
  481. };
  482. }
  483. function afterGeomDraw(chart) {
  484. var controller = chart.get('pieLabelController');
  485. if (controller.pieLabelCfg) {
  486. // 用户配置了饼图文本
  487. controller.renderLabels();
  488. controller.bindEvents(); // 绑定事件
  489. }
  490. }
  491. function clearInner(chart) {
  492. var controller = chart.get('pieLabelController');
  493. if (controller.pieLabelCfg) {
  494. // 用户配置了饼图文本
  495. controller.clear();
  496. }
  497. }
  498. export { init, afterGeomDraw, clearInner };
  499. export default {
  500. init,
  501. afterGeomDraw,
  502. clearInner
  503. };