UNPKG

10.1 kBTypeScriptView Raw
1import { jsx } from '../../jsx';
2import Component from '../../base/component';
3import { deepMix, isArray, isFunction } from '@antv/util';
4import { isInBBox, getElementsByClassName } from '../../util';
5import { Ref, Point } from '../../types';
6
7const DEFAULT_CONFIG = {
8 anchorOffset: '10px', // 锚点的偏移量
9 inflectionOffset: '30px', // 拐点的偏移量
10 sidePadding: '15px', // 文本距离画布四边的距离
11 height: '64px', // 文本的行高
12 adjustOffset: '30', // 发生调整时的偏移量
13 triggerOn: 'click', // 点击行为触发的时间类型
14 // activeShape: false, // 当有图形被选中的时候,是否激活图形
15 // activeStyle: {
16 // offset: '1px',
17 // appendRadius: '8px',
18 // fillOpacity: 0.5,
19 // },
20 label1OffsetY: '-4px',
21 label2OffsetY: '4px',
22};
23
24function getEndPoint(center: Point, angle: number, r: number) {
25 return {
26 x: center.x + r * Math.cos(angle),
27 y: center.y + r * Math.sin(angle),
28 };
29}
30
31// 计算中间角度
32function getMiddleAngle(startAngle: number, endAngle: number) {
33 if (endAngle < startAngle) {
34 endAngle += Math.PI * 2;
35 }
36 return (endAngle + startAngle) / 2;
37}
38
39function move(from, to, count, center) {
40 const { x } = center;
41 const sort = from.sort((a, b) => {
42 const aDistance = Math.abs(a.x - x);
43 const bDistance = Math.abs(b.x - x);
44 return bDistance - aDistance;
45 });
46 return [sort.slice(0, sort.length - count), sort.slice(sort.length - count).concat(to)];
47}
48
49// 第一象限
50function isFirstQuadrant(angle: number) {
51 return angle >= -Math.PI / 2 && angle < 0;
52}
53// 第二象限
54function isSecondQuadrant(angle: number) {
55 return angle >= 0 && angle < Math.PI / 2;
56}
57
58function isThirdQuadrant(angle: number) {
59 return angle >= Math.PI / 2 && angle < Math.PI;
60}
61function isFourthQuadrant(angle: number) {
62 return angle >= Math.PI && angle < (Math.PI * 3) / 2;
63}
64
65function findShapeByClassName(shape, point, className) {
66 const targetShapes = getElementsByClassName(className, shape);
67 for (let i = 0, len = targetShapes.length; i < len; i++) {
68 const shape = targetShapes[i];
69 if (isInBBox(shape.getBBox(), point)) {
70 return shape;
71 }
72 }
73}
74
75export default (View) => {
76 return class PieLabel extends Component {
77 triggerRef: Ref;
78 labels: [];
79 constructor(props) {
80 super(props);
81 this.triggerRef = {};
82 }
83
84 willMount() {}
85
86 /**
87 * 绑定事件
88 */
89 didMount() {
90 this._initEvent();
91 }
92
93 getLabels(props) {
94 const {
95 chart,
96 coord,
97 anchorOffset,
98 inflectionOffset,
99 label1,
100 label2,
101 height: itemHeight,
102 sidePadding
103 } = props;
104
105 const {
106 center,
107 radius,
108 width: coordWidth,
109 height: coordHeight,
110 left: coordLeft,
111 right: coordRight,
112 top: coordTop,
113 } = coord;
114
115 const maxCountForOneSide = Math.floor(coordHeight / itemHeight);
116 const maxCount = maxCountForOneSide * 2;
117
118 const geometry = chart.getGeometrys()[0];
119 const records = geometry
120 .flatRecords()
121 // 按角度大到小排序
122 .sort((a, b) => {
123 const angle1 = a.xMax - a.xMin;
124 const angle2 = b.xMax - b.xMin;
125 return angle2 - angle1;
126 })
127 // 只取前 maxCount 个显示
128 .slice(0, maxCount);
129
130 // 存储左右 labels
131 let halves = [
132 [], // left
133 [], // right
134 ];
135 records.forEach((record) => {
136 const { xMin, xMax, color, origin } = record;
137
138 // 锚点角度
139 const anchorAngle = getMiddleAngle(xMin, xMax);
140 // 锚点坐标
141 const anchorPoint = getEndPoint(center, anchorAngle, radius + anchorOffset);
142 // 拐点坐标
143 const inflectionPoint = getEndPoint(center, anchorAngle, radius + inflectionOffset);
144 // 锚点方向
145 const side = anchorPoint.x < center.x ? 'left' : 'right';
146
147 const label = {
148 origin,
149 angle: anchorAngle,
150 anchor: anchorPoint,
151 inflection: inflectionPoint,
152 side,
153 x: inflectionPoint.x,
154 y: inflectionPoint.y,
155 r: radius + inflectionOffset,
156 color,
157 label1: isFunction(label1) ? label1(origin, record) : label1,
158 label2: isFunction(label2) ? label2(origin, record) : label2,
159 };
160
161 // 判断文本的方向
162 if (side === 'left') {
163 halves[0].push(label);
164 } else {
165 halves[1].push(label);
166 }
167 });
168
169 // 判断是有一边超过了显示的最大
170 if (halves[0].length > maxCountForOneSide) {
171 halves = move(halves[0], halves[1], halves[0].length - maxCountForOneSide, center);
172 } else if (halves[1].length > maxCountForOneSide) {
173 const [right, left] = move(
174 halves[1],
175 halves[0],
176 halves[1].length - maxCountForOneSide,
177 center
178 );
179 halves = [left, right];
180 }
181
182 // label 的最大宽度
183 const labelWidth = coordWidth / 2 - radius - anchorOffset - inflectionOffset - 2 * sidePadding;
184 const labels = [];
185 halves.forEach((half, index) => {
186 const showSide = index === 0 ? 'left' : 'right';
187
188 // 顺时针方向排序
189 half.sort((a, b) => {
190 let aAngle = a.angle;
191 let bAngle = b.angle;
192 if (showSide === 'left') {
193 // 是否在第一象限
194 aAngle = isFirstQuadrant(aAngle) ? aAngle + Math.PI * 2 : aAngle;
195 bAngle = isFirstQuadrant(bAngle) ? bAngle + Math.PI * 2 : bAngle;
196 return bAngle - aAngle;
197 } else {
198 // 是否在第四象限
199 aAngle = isFourthQuadrant(aAngle) ? aAngle - Math.PI * 2 : aAngle;
200 bAngle = isFourthQuadrant(bAngle) ? bAngle - Math.PI * 2 : bAngle;
201 return aAngle - bAngle;
202 }
203 });
204
205 const pointsY = half.map((label) => label.y);
206 const maxY = Math.max.apply(null, pointsY);
207 const minY = Math.min.apply(null, pointsY);
208
209 // 每个 label 占用的高度
210 const labelCount = half.length;
211 const labelHeight = coordHeight / labelCount;
212 const halfLabelHeight = labelHeight / 2;
213 // 线之间的间隔
214 const lineInterval = 2;
215
216 if (showSide === 'left') {
217 half.forEach((label, index) => {
218 const { anchor, inflection, angle, x, y } = label;
219
220 const points = [anchor, inflection];
221 const endX = coordLeft + sidePadding;
222 const endY = coordTop + halfLabelHeight + labelHeight * index;
223
224 // 文本开始点
225 const labelStart = {
226 x: endX + labelWidth + lineInterval * index,
227 y: endY,
228 };
229 // 文本结束点
230 const labelEnd = { x: endX, y: endY };
231
232 // 第四象限
233 if (isFirstQuadrant(angle)) {
234 const pointY = minY - lineInterval * (labelCount - index);
235 points.push({ x, y: pointY });
236 points.push({ x: labelStart.x, y: pointY });
237 } else if (isThirdQuadrant(angle) || isFourthQuadrant(angle)) {
238 points.push({ x: labelStart.x, y });
239 } else if (isSecondQuadrant(angle)) {
240 const pointY = maxY + lineInterval * index;
241 points.push({ x, y: pointY });
242 points.push({ x: labelStart.x, y: pointY });
243 }
244
245 points.push(labelStart);
246 points.push(labelEnd);
247
248 label.points = points;
249 label.side = showSide;
250
251 labels.push(label);
252 });
253 } else {
254 half.forEach((label, index) => {
255 const { anchor, inflection, angle, x, y } = label;
256
257 // 折线的点
258 const points = [anchor, inflection];
259 const endX = coordRight - sidePadding;
260 const endY = coordTop + halfLabelHeight + labelHeight * index;
261
262 // 文本开始点
263 const labelStart = {
264 x: endX - labelWidth - lineInterval * index,
265 y: endY,
266 };
267 // 文本结束点
268 const labelEnd = { x: endX, y: endY };
269
270 // 第四象限
271 if (isFourthQuadrant(angle)) {
272 const pointY = minY - lineInterval * (labelCount - index);
273 points.push({ x, y: pointY });
274 points.push({ x: labelStart.x, y: pointY });
275 } else if (isFirstQuadrant(angle) || isSecondQuadrant(angle)) {
276 points.push({ x: labelStart.x, y });
277 } else if (isThirdQuadrant(angle)) {
278 const pointY = maxY + lineInterval * index;
279 points.push({ x, y: pointY });
280 points.push({ x: labelStart.x, y: pointY });
281 }
282
283 points.push(labelStart);
284 points.push(labelEnd);
285
286 label.points = points;
287 label.side = showSide;
288 labels.push(label);
289 });
290 }
291 });
292
293 return labels;
294 }
295
296 _handleEvent = (ev) => {
297 const { chart, onClick } = this.props;
298 const ele = this.triggerRef.current;
299 const point = ev.points[0];
300
301 const shape = findShapeByClassName(ele, point, 'click');
302 const pieData = chart.getSnapRecords(point);
303
304 if (typeof onClick === 'function') {
305 // 点击label
306 if (shape) {
307 onClick(shape.get('data'));
308 }
309 // 点击饼图
310 else if (isArray(pieData) && pieData.length > 0) {
311 onClick(pieData);
312 }
313 }
314 };
315
316 _initEvent() {
317 const { context, props } = this;
318 const { canvas } = context;
319 const { triggerOn = DEFAULT_CONFIG.triggerOn } = props;
320
321 canvas.on(triggerOn, this._handleEvent);
322 }
323
324 render() {
325 const { context } = this;
326 const props = context.px2hd(deepMix({}, DEFAULT_CONFIG, this.props));
327 const labels = this.getLabels(props);
328 return <View labels={labels} {...props} triggerRef={this.triggerRef} />;
329 }
330 };
331};