UNPKG

16.9 kBTypeScriptView Raw
1import { isFunction, each, upperFirst, mix, groupToMap, isObject, flatten } from '@antv/util';
2import Selection, { SelectionState } from './selection';
3import { Adjust, getAdjust } from '@antv/adjust';
4import { toTimeStamp } from '../../util/index';
5import { GeomType, GeometryProps, GeometryAdjust } from './interface';
6import AttrController from '../../controller/attr';
7import equal from '../../base/equal';
8import { AnimationCycle } from '../../canvas/animation/interface';
9import { Scale } from '@antv/scale';
10
11// 保留原始数据的字段
12const FIELD_ORIGIN = 'origin';
13
14export interface AdjustProp {
15 type: string;
16 adjust: Adjust;
17}
18
19class Geometry<
20 P extends GeometryProps = GeometryProps,
21 S extends SelectionState = SelectionState
22> extends Selection<P, S> {
23 isGeometry = true;
24 geomType: GeomType;
25
26 attrs: any;
27 adjust: AdjustProp;
28
29 // 预处理后的数据
30 dataArray: any;
31 records: any[];
32 mappedArray: any;
33 // x 轴居中
34 justifyContent = false;
35 // y 轴是否从0开始
36 startOnZero = false;
37 // 是否连接空值
38 connectNulls: boolean = false;
39 // 是否需要排序
40 sortable: boolean = false;
41 attrController: AttrController;
42
43 // 动画配置
44 animation: AnimationCycle;
45
46 getDefaultCfg() {
47 return {};
48 }
49
50 constructor(props: P, context?) {
51 super(props, context);
52 mix(this, this.getDefaultCfg());
53
54 const { chart, coord } = props;
55
56 const attrsRange = this._getThemeAttrsRange();
57 this.attrController = new AttrController(chart.scale, attrsRange);
58 const { attrController, justifyContent } = this;
59
60 const attrOptions = attrController.getAttrOptions(props, !coord.isCyclic() || justifyContent);
61 attrController.create(attrOptions);
62 }
63
64 willReceiveProps(nextProps) {
65 super.willReceiveProps(nextProps);
66 const { props: lastProps, attrController, justifyContent } = this;
67 const { data: nextData, adjust: nextAdjust, zoomRange: nextZoomRange, coord } = nextProps;
68 const { data: lastData, adjust: lastAdjust, zoomRange: lastZoomRange } = lastProps;
69
70 const justifyContentCenter = !coord.isCyclic() || justifyContent;
71
72 const nextAttrOptions = attrController.getAttrOptions(nextProps, justifyContentCenter);
73 const lastAttrOptions = attrController.getAttrOptions(lastProps, justifyContentCenter);
74 if (!equal(nextAttrOptions, lastAttrOptions)) {
75 attrController.update(nextAttrOptions);
76 this.records = null;
77 }
78
79 // 重新处理数据
80 if (nextData !== lastData) {
81 this.records = null;
82 }
83
84 // 重新处理数据
85 if (nextAdjust !== lastAdjust) {
86 this.records = null;
87 }
88
89 // zoomRange发生变化,records也需要重新计算
90 if (!equal(nextZoomRange, lastZoomRange)) {
91 this.records = null;
92 }
93 }
94
95 willMount() {
96 this._createAttrs();
97 if (!this.records) {
98 this._processData();
99 }
100 }
101 willUpdate() {
102 this._createAttrs();
103 if (!this.records) {
104 this._processData();
105 }
106 }
107
108 didMount() {
109 super.didMount();
110 this._initEvent();
111 }
112
113 _createAttrs() {
114 const { attrController } = this;
115 attrController.attrs = {};
116 this.attrs = attrController.getAttrs();
117 }
118
119 _getThemeAttrsRange() {
120 const { context, props, geomType } = this;
121 const { coord } = props;
122 const { theme } = context;
123 const { colors, sizes, shapes } = theme;
124
125 return {
126 x: coord.x,
127 y: coord.y,
128 color: colors,
129 size: sizes,
130 shape: shapes[geomType],
131 };
132 }
133
134 _adjustScales() {
135 const { attrs, props, startOnZero: defaultStartOnZero } = this;
136 const { chart, startOnZero = defaultStartOnZero, coord, adjust } = props;
137 const { isPolar, transposed } = coord;
138 const { y } = attrs;
139 const yField = y.field;
140 // 如果从 0 开始,只调整 y 轴 scale
141 if (startOnZero) {
142 const { y } = attrs;
143 chart.scale.adjustStartZero(y.scale);
144 }
145 // 饼图的scale调整,关闭nice
146 if (
147 isPolar &&
148 transposed &&
149 (adjust === 'stack' || (adjust as GeometryAdjust)?.type === 'stack')
150 ) {
151 const { y } = attrs;
152 chart.scale.adjustPieScale(y.scale);
153 }
154
155 if (adjust === 'stack' || (adjust as GeometryAdjust)?.type === 'stack') {
156 this._updateStackRange(yField, y.scale, this.dataArray);
157 }
158 }
159
160 _groupData(data) {
161 const { attrController } = this;
162 const groupScales = attrController.getGroupScales();
163 if (!groupScales.length) {
164 return [{ children: data }];
165 }
166
167 const names = [];
168 groupScales.forEach((scale) => {
169 const field = scale.field;
170 names.push(field);
171 });
172 const groups = groupToMap(data, names);
173 const records = [];
174 for (const key in groups) {
175 records.push({
176 key: key.replace(/^_/, ''),
177 children: groups[key],
178 });
179 }
180 return records;
181 }
182
183 _saveOrigin(originData) {
184 const len = originData.length;
185 const data = new Array(len);
186 for (let i = 0; i < len; i++) {
187 const record = originData[i];
188 data[i] = {
189 ...record,
190 [FIELD_ORIGIN]: record,
191 };
192 }
193 return data;
194 }
195
196 _numberic(data) {
197 const { attrs } = this;
198 const scales = [attrs.x.scale, attrs.y.scale];
199 for (let j = 0, len = data.length; j < len; j++) {
200 const obj = data[j];
201 const count = scales.length;
202 for (let i = 0; i < count; i++) {
203 const scale = scales[i];
204 if (scale.isCategory) {
205 const field = scale.field;
206 obj[field] = scale.translate(obj[field]);
207 }
208 }
209 }
210 }
211
212 _adjustData(records) {
213 const { attrs, props } = this;
214 const { adjust } = props;
215
216 // groupedArray 是二维数组
217 const groupedArray = records.map((record) => record.children);
218
219 if (!adjust) {
220 return groupedArray;
221 }
222 const adjustCfg =
223 typeof adjust === 'string'
224 ? {
225 type: adjust,
226 }
227 : adjust;
228 const adjustType = upperFirst(adjustCfg.type);
229 const AdjustConstructor = getAdjust(adjustType);
230 if (!AdjustConstructor) {
231 throw new Error('not support such adjust : ' + adjust);
232 }
233
234 if (adjustType === 'Dodge') {
235 for (let i = 0, len = groupedArray.length; i < len; i++) {
236 // 如果是dodge, 需要处理数字再处理
237 this._numberic(groupedArray[i]);
238 }
239 adjustCfg.adjustNames = ['x'];
240 }
241
242 const { x, y } = attrs;
243 adjustCfg.xField = x.field;
244 adjustCfg.yField = y.field;
245
246 const adjustInstance = new AdjustConstructor(adjustCfg);
247 const adjustData = adjustInstance.process(groupedArray);
248
249 this.adjust = {
250 type: adjustCfg.type,
251 adjust: adjustInstance,
252 };
253
254 // process 返回的是新数组,所以要修改 records
255 records.forEach((record, index: number) => {
256 record.children = adjustData[index];
257 });
258
259 return adjustData;
260 }
261
262 _updateStackRange(field, scale, dataArray) {
263 const flattenArray = flatten(dataArray);
264 let min = Infinity;
265 let max = -Infinity;
266 for (let i = 0, len = flattenArray.length; i < len; i++) {
267 const obj = flattenArray[i];
268 const tmpMin = Math.min.apply(null, obj[field]);
269 const tmpMax = Math.max.apply(null, obj[field]);
270 if (tmpMin < min) {
271 min = tmpMin;
272 }
273 if (tmpMax > max) {
274 max = tmpMax;
275 }
276 }
277 if (min !== scale.min || max !== scale.max) {
278 scale.change({
279 min,
280 max,
281 });
282 }
283 }
284
285 _processData() {
286 const { props } = this;
287 const { data: originData } = props;
288
289 const data = this._saveOrigin(originData);
290 // 根据分类度量进行数据分组
291 const records = this._groupData(data);
292 // 根据adjust分组
293 const dataArray = this._adjustData(records);
294
295 this.dataArray = dataArray;
296
297 // scale适配调整,主要是调整 y 轴是否从 0 开始 以及 饼图
298 this._adjustScales();
299
300 // 数据排序(非必须)
301 if (this.sortable) {
302 this._sortData(records);
303 }
304
305 this.records = records;
306 }
307
308 _sortData(records) {
309 const xScale = this.getXScale();
310 const { field, type } = xScale;
311 if (type !== 'identity' && xScale.values.length > 1) {
312 each(records, ({ children }) => {
313 children.sort((record1, record2) => {
314 if (type === 'timeCat') {
315 return (
316 toTimeStamp(record1[FIELD_ORIGIN][field]) - toTimeStamp(record2[FIELD_ORIGIN][field])
317 );
318 }
319 return (
320 xScale.translate(record1[FIELD_ORIGIN][field]) -
321 xScale.translate(record2[FIELD_ORIGIN][field])
322 );
323 });
324 });
325 }
326 }
327
328 _initEvent() {
329 const { container, props } = this;
330 const canvas = container.get('canvas');
331 ['onPressStart', 'onPress', 'onPressEnd', 'onPan', 'onPanStart', 'onPanEnd'].forEach(
332 (eventName) => {
333 if (props[eventName]) {
334 canvas.on(eventName.substr(2).toLowerCase(), (ev) => {
335 ev.geometry = this;
336 props[eventName](ev);
337 });
338 }
339 }
340 );
341 }
342
343 getY0Value() {
344 const { attrs, props } = this;
345 const { chart } = props;
346 const { field } = attrs.y;
347 const scale = chart.getScale(field);
348 return chart.scale.getZeroValue(scale);
349 }
350
351 // 根据各属性映射的值域来获取真正的绘图属性
352 _getShapeStyle(shape, origin) {
353 const { context, props, geomType } = this;
354 const { theme } = context;
355 const shapeTheme = theme.shape[geomType] || {};
356 const defaultShapeStyle = shapeTheme.default;
357 const shapeThemeStyle = shapeTheme[shape];
358 const { style } = props;
359
360 const shapeStyle = {
361 ...defaultShapeStyle,
362 ...shapeThemeStyle,
363 };
364 if (!style || !isObject(style)) {
365 return shapeStyle;
366 }
367 // @ts-ignore
368 const { field, ...styles } = style;
369 const value = field ? origin[field] : origin;
370 each(styles, (attr, key) => {
371 if (isFunction(attr)) {
372 shapeStyle[key] = attr(value);
373 } else {
374 shapeStyle[key] = attr;
375 }
376 });
377 return shapeStyle;
378 }
379
380 /**
381 * 数据映射到视图属性核心逻辑
382 * x、y 每个元素走 normalize 然后 convertPoint
383 * color、size、shape
384 * 如果是Linear,则每个元素 走 mapping
385 * 如果是Category/Identity 则第一个元素走 mapping
386 */
387 _mapping(records) {
388 const { attrs, props, attrController } = this;
389 const { coord } = props;
390
391 const { linearAttrs, nonlinearAttrs } = attrController.getAttrsByLinear();
392 const defaultAttrValues = attrController.getDefaultAttrValues();
393
394 for (let i = 0, len = records.length; i < len; i++) {
395 const record = records[i];
396 const { children } = record;
397 const attrValues = {
398 ...defaultAttrValues,
399 };
400 const firstChild = children[0];
401 if (children.length === 0) {
402 continue;
403 }
404 // 非线性映射
405 for (let k = 0, len = nonlinearAttrs.length; k < len; k++) {
406 const attrName = nonlinearAttrs[k];
407 const attr = attrs[attrName];
408 // 非线性映射只用映射第一项就可以了
409 attrValues[attrName] = attr.mapping(firstChild[attr.field]);
410 }
411
412 // 线性属性映射
413 for (let j = 0, childrenLen = children.length; j < childrenLen; j++) {
414 const child = children[j];
415 const normalized: any = {};
416 for (let k = 0; k < linearAttrs.length; k++) {
417 const attrName = linearAttrs[k];
418 const attr = attrs[attrName];
419 // 分类属性的线性映射
420 if (attrController.isGroupAttr(attrName)) {
421 attrValues[attrName] = attr.mapping(child[attr.field], child);
422 } else {
423 normalized[attrName] = attr.normalize(child[attr.field]);
424 }
425 }
426
427 const { x, y } = coord.convertPoint({
428 x: normalized.x,
429 y: normalized.y,
430 });
431
432 // 获取 shape 的 style
433 const shapeName = attrValues.shape;
434 const shape = this._getShapeStyle(shapeName, child.origin);
435 const selected = this.isSelected(child);
436
437 mix(child, attrValues, {
438 normalized,
439 x,
440 y,
441 shapeName,
442 shape,
443 selected,
444 });
445 }
446 }
447 return records;
448 }
449
450 // 数据映射
451 mapping() {
452 const { records } = this;
453 // 数据映射
454 this._mapping(records);
455
456 return records;
457 }
458
459 getClip() {
460 const { coord, viewClip } = this.props;
461 const { width: contentWidth, height: contentHeight, left, top } = coord;
462 if (viewClip) {
463 return {
464 type: 'rect',
465 attrs: {
466 x: left,
467 y: top,
468 width: contentWidth,
469 height: contentHeight,
470 },
471 };
472 }
473 return null;
474 }
475
476 getAttr(attrName: string) {
477 return this.attrController.getAttr(attrName);
478 }
479
480 getXScale(): Scale {
481 return this.getAttr('x').scale;
482 }
483
484 getYScale(): Scale {
485 return this.getAttr('y').scale;
486 }
487
488 _getXSnap(invertPointX) {
489 const xScale = this.getXScale();
490 if (xScale.isCategory) {
491 return xScale.invert(invertPointX);
492 }
493
494 // linear 类型
495 const invertValue = xScale.invert(invertPointX);
496 const values = xScale.values;
497 const len = values.length;
498 // 如果只有1个点直接返回第1个点
499 if (len === 1) {
500 return values[0];
501 }
502 // 第1个点和第2个点之间
503 if ((values[0] + values[1]) / 2 > invertValue) {
504 return values[0];
505 }
506 // 最后2个点
507 if ((values[len - 2] + values[len - 1]) / 2 <= invertValue) {
508 return values[len - 1];
509 }
510 for (let i = 1; i < len; i++) {
511 // 中间的点
512 if (
513 (values[i - 1] + values[i]) / 2 <= invertValue &&
514 (values[i + 1] + values[i]) / 2 > invertValue
515 ) {
516 return values[i];
517 }
518 }
519 return null;
520 }
521
522 _getYSnapRecords(invertPointY, records) {
523 const yScale = this.getYScale();
524 const { field: yField } = yScale;
525 const yValue = yScale.invert(invertPointY);
526 // category
527 if (yScale.isCategory) {
528 return records.filter((record) => record[FIELD_ORIGIN][yField] === yValue);
529 }
530 // linear
531 return records.filter((record) => {
532 const rangeY = record[yField];
533 if (rangeY[0] <= yValue && rangeY[1] >= yValue) {
534 return true;
535 }
536 return false;
537 });
538 }
539
540 // 把 records 拍平
541 flatRecords() {
542 const { records } = this;
543 return records.reduce((prevRecords, record) => {
544 return prevRecords.concat(record.children);
545 }, []);
546 }
547
548 getSnapRecords(point, inCoordRange?): any[] {
549 const { props } = this;
550 const { coord, adjust } = props;
551 const invertPoint = coord.invertPoint(point);
552 const xScale = this.getXScale();
553 const yScale = this.getYScale();
554
555 // 如果不在coord坐标范围内,直接返回空
556 // if (invertPoint.x < 0 || invertPoint.y < 0) {
557 // return [];
558 // }
559
560 // 是否调整 point,默认为不调整
561 if (inCoordRange) {
562 const { range: xRange } = xScale;
563 const { range: yRange } = yScale;
564 // 如果 inCoordRange=true,当 point 不在 coord 坐标范围内时,调整到 range 内
565 invertPoint.x = Math.min(Math.max(invertPoint.x, xRange[0]), xRange[1]);
566 invertPoint.y = Math.min(Math.max(invertPoint.y, yRange[0]), yRange[1]);
567 }
568
569 const records = this.flatRecords();
570
571 // 处理饼图
572 if (adjust === 'stack' && coord.isPolar && coord.transposed) {
573 // 弧度在半径范围内
574 if (invertPoint.x >= 0 && invertPoint.x <= 1) {
575 const snapRecords = this._getYSnapRecords(invertPoint.y, records);
576 return snapRecords;
577 }
578 }
579
580 const rst = [];
581 const value = this._getXSnap(invertPoint.x);
582 if (!value) {
583 return rst;
584 }
585 const { field: xField } = xScale;
586 const { field: yField } = yScale;
587 for (let i = 0, len = records.length; i < len; i++) {
588 const record = {
589 ...records[i],
590 xField,
591 yField,
592 };
593 const originValue = record[FIELD_ORIGIN][xField];
594 if (xScale.type === 'timeCat' && toTimeStamp(originValue) === value) {
595 rst.push(record);
596 } else if (originValue === value) {
597 rst.push(record);
598 }
599 }
600
601 return rst;
602 }
603
604 getLegendItems() {
605 const { attrController } = this;
606 const colorAttr = attrController.getAttr('color');
607 if (!colorAttr) return null;
608 const { scale } = colorAttr;
609 if (!scale.isCategory) return null;
610 const ticks = scale.getTicks();
611 const items = ticks.map((tick) => {
612 const { text, tickValue } = tick;
613 const color = colorAttr.mapping(tickValue);
614 return {
615 field: scale.field,
616 color,
617 name: text, // for display
618 tickValue,
619 };
620 });
621 return items;
622 }
623}
624
625export default Geometry;