pie-label.js 14 KB

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