1 | import { isFunction, each, upperFirst, mix, groupToMap, isObject, flatten } from '@antv/util';
|
2 | import Selection, { SelectionState } from './selection';
|
3 | import { Adjust, getAdjust } from '@antv/adjust';
|
4 | import { toTimeStamp } from '../../util/index';
|
5 | import { GeomType, GeometryProps, GeometryAdjust } from './interface';
|
6 | import AttrController from '../../controller/attr';
|
7 | import equal from '../../base/equal';
|
8 | import { AnimationCycle } from '../../canvas/animation/interface';
|
9 | import { Scale } from '@antv/scale';
|
10 |
|
11 |
|
12 | const FIELD_ORIGIN = 'origin';
|
13 |
|
14 | export interface AdjustProp {
|
15 | type: string;
|
16 | adjust: Adjust;
|
17 | }
|
18 |
|
19 | class 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 |
|
34 | justifyContent = false;
|
35 |
|
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 |
|
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 |
|
141 | if (startOnZero) {
|
142 | const { y } = attrs;
|
143 | chart.scale.adjustStartZero(y.scale);
|
144 | }
|
145 |
|
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 |
|
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 |
|
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 |
|
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 |
|
293 | const dataArray = this._adjustData(records);
|
294 |
|
295 | this.dataArray = dataArray;
|
296 |
|
297 |
|
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 |
|
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 |
|
383 |
|
384 |
|
385 |
|
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 |
|
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 |
|
495 | const invertValue = xScale.invert(invertPointX);
|
496 | const values = xScale.values;
|
497 | const len = values.length;
|
498 |
|
499 | if (len === 1) {
|
500 | return values[0];
|
501 | }
|
502 |
|
503 | if ((values[0] + values[1]) / 2 > invertValue) {
|
504 | return values[0];
|
505 | }
|
506 |
|
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 |
|
527 | if (yScale.isCategory) {
|
528 | return records.filter((record) => record[FIELD_ORIGIN][yField] === yValue);
|
529 | }
|
530 |
|
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 |
|
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 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 | if (inCoordRange) {
|
562 | const { range: xRange } = xScale;
|
563 | const { range: yRange } = yScale;
|
564 |
|
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,
|
618 | tickValue,
|
619 | };
|
620 | });
|
621 | return items;
|
622 | }
|
623 | }
|
624 |
|
625 | export default Geometry;
|