import { isFunction, each, upperFirst, mix, groupToMap, isObject, flatten, isNull, find, } from '@antv/util'; import { ChartChildProps } from '../../chart'; import Selection, { SelectionProps, SelectionState } from './selection'; import { Adjust, Dodge, Jitter, Stack, Symmetric } from '../../deps/f2-adjust/src'; import { toTimeStamp } from '../../util/index'; import AttrController, { ATTRS } from '../../controller/attr'; import { Scale } from '../../deps/f2-scale/src'; import { AnimationProps, isEqual } from '@antv/f-engine'; import { AdjustType, AdjustProps } from './Adjust'; import { DataRecord, DataField } from '../../chart/Data'; const AdjustMap = { Stack: Stack, Dodge: Dodge, Jitter: Jitter, Symmetric: Symmetric, }; // 保留原始数据的字段 const FIELD_ORIGIN = 'origin'; const OVERRIDE_FIELDS_SET = new Set([ 'color', 'normalized', 'x', 'y', 'shapeName', 'shape', 'selected', ]); export type GeometryType = 'line' | 'point' | 'area' | 'polygon' | 'schema' | 'interval'; export interface ColorAttrObject { type?: string; field?: string; range?: any[]; callback?: (...args) => any; scale?: any; } export interface SizeAttrObject { type?: string; field?: string; range?: any[]; callback?: (...args) => any; scale?: any; } export interface ShapeAttrObject { type?: string; field?: string; range?: any[]; callback?: (...args) => any; scale?: any; } export interface GeometryProps extends SelectionProps { x: DataField; y: DataField; color?: DataField | string | [string, any[]] | ColorAttrObject; size?: DataField | number | string | [string, any[]] | SizeAttrObject; shape?: DataField | number | string | [string, any[]] | ShapeAttrObject; adjust?: AdjustType | AdjustProps; startOnZero?: boolean; style?: any; animation?: AnimationProps; /** * 是否裁剪显示区 */ viewClip?: boolean; onPress?: Function; onPan?: Function; onPressStart?: Function; onPressEnd?: Function; onPanStart?: Function; onPanEnd?: Function; } class Geometry< TRecord extends DataRecord = DataRecord, P extends GeometryProps = GeometryProps, S extends SelectionState = SelectionState > extends Selection

{ isGeometry = true; geomType: GeometryType; attrs: any; adjust: AdjustProps & { adjust: Adjust }; // 预处理后的数据 dataArray: any; // data 预处理后,records mapping 前的数据 dataRecords: any[]; records: any[]; mappedArray: any; // x 轴居中 justifyContent = false; // y 轴是否从0开始 startOnZero = false; // 是否连接空值 connectNulls: boolean = false; // 是否需要排序 sortable: boolean = false; attrController: AttrController; // 动画配置 animation: AnimationProps; getDefaultCfg() { return {}; } constructor(props: P & ChartChildProps, context?) { super(props, context); mix(this, this.getDefaultCfg()); const { chart } = props; const attrsRange = this._getThemeAttrsRange(); this.attrController = new AttrController(chart.scale, attrsRange); const { attrController } = this; const attrOptions = this.getAttrOptions(props); attrController.create(attrOptions); } getAttrOptions(props) { const { coord } = props; const { attrController, justifyContent } = this; const justifyContentCenter = !coord.isCyclic() || justifyContent; const args = {}; ATTRS.forEach((d) => (args[d] = props[d])); const attrOptions = attrController.getAttrOptions( this.context.px2hd(args), justifyContentCenter ); return attrOptions; } willReceiveProps(nextProps) { const { props: lastProps, attrController } = this; const { data: nextData, adjust: nextAdjust, selection } = nextProps; const { data: lastData, adjust: lastAdjust, selection: lastSelection } = lastProps; const lastAttrOptions = this.getAttrOptions(lastProps); attrController.attrsRange = this._getThemeAttrsRange(); const nextAttrOptions = this.getAttrOptions(nextProps); if (!isEqual(nextAttrOptions, lastAttrOptions)) { attrController.update(nextAttrOptions); this.dataRecords = null; } // 重新处理数据 if (nextData !== lastData) { this.dataRecords = null; } // 重新处理数据 if (nextAdjust !== lastAdjust) { this.dataRecords = null; } // selection 发生变化 if (!isEqual(selection, lastSelection)) { super.willReceiveProps(nextProps); } } willMount() { this._createAttrs(); if (!this.dataRecords) { this._processData(); } } willUpdate() { this._createAttrs(); if (!this.dataRecords) { this._processData(); } else { this._readjustData(this.dataRecords); } } didMount() { this._initEvent(); super.didMount(); // 更新 attrController this.attrController.attrsRange = this._getThemeAttrsRange(); } _initEvent() { const { props } = this; const { chart } = props; ['onPressStart', 'onPress', 'onPressEnd', 'onPan', 'onPanStart', 'onPanEnd'].forEach( (eventName) => { if (props[eventName]) { chart.on(eventName.substr(2).toLowerCase(), (ev) => { ev.geometry = this; props[eventName](ev); }); } } ); } _createAttrs() { const { attrController } = this; attrController.attrs = {}; this.attrs = attrController.getAttrs(); } _getThemeAttrsRange() { const { context, props, geomType } = this; const { coord } = props; const { theme } = context; const { colors, sizes, shapes } = theme; return { x: coord.x, y: coord.y, color: colors, size: sizes, shape: shapes[geomType], }; } _createAdjust() { const { attrs, props } = this; const { adjust, chart } = props; if (!adjust) { return null; } const adjustCfg: AdjustProps = typeof adjust === 'string' ? { type: adjust, } : adjust; const adjustType = upperFirst(adjustCfg.type); const AdjustConstructor = AdjustMap[adjustType]; if (!AdjustConstructor) { throw new Error('not support such adjust : ' + adjust); } if (adjustType === 'Dodge') { // @ts-ignore adjustCfg.adjustNames = ['x']; } const { x, y } = attrs; // @ts-ignore adjustCfg.xField = x.field; // @ts-ignore adjustCfg.yField = y.field; const adjustInstance = new AdjustConstructor(adjustCfg); this.adjust = { type: adjustCfg.type, adjust: adjustInstance, }; chart.updateAdjust(this.adjust); return this.adjust; } _adjustScales() { const { attrs, props, startOnZero: defaultStartOnZero } = this; const { chart, startOnZero = defaultStartOnZero, coord, adjust } = props; const { isPolar, transposed } = coord; const { y } = attrs; // 如果从 0 开始,只调整 y 轴 scale if (startOnZero) { chart.scale.adjustStartZero(y.scale); } // 饼图的scale调整,关闭nice if ( isPolar && transposed && (adjust === 'stack' || (adjust as AdjustProps)?.type === 'stack') ) { chart.scale.adjustPieScale(y.scale); } if (adjust === 'stack' || (adjust as AdjustProps)?.type === 'stack') { chart.scale._updateStackRange(y.scale, flatten(this.dataArray)); } } _groupData(data) { const { attrController } = this; const groupScales = attrController.getGroupScales(); if (!groupScales.length) { return [{ children: data }]; } const names = []; groupScales.forEach((scale) => { const field = scale.field; names.push(field); }); const groups = groupToMap(data, names); const records = []; for (const key in groups) { records.push({ key: key.replace(/^_/, ''), children: groups[key], }); } return records; } _saveOrigin(originData) { const len = originData.length; const data = new Array(len); for (let i = 0; i < len; i++) { const record = originData[i]; data[i] = { ...record, [FIELD_ORIGIN]: record, }; } return data; } _numberic(data) { const { attrs } = this; const scales = [attrs.x.scale, attrs.y.scale]; for (let j = 0, len = data.length; j < len; j++) { const obj = data[j]; const count = scales.length; for (let i = 0; i < count; i++) { const scale = scales[i]; if (scale.isCategory) { const field = scale.field; const value = scale.translate(obj.origin[field]); obj[field] = value; } } } } _adjustData(records) { const { adjust } = this; // groupedArray 是二维数组 const groupedArray = records.map((record) => record.children); if (!adjust) { return groupedArray; } const { attrs } = this; const scales = [attrs.x.scale, attrs.y.scale]; for (let i = 0, len = groupedArray.length; i < len; i++) { const records = groupedArray[i]; for (let j = 0, len = records.length; j < len; j++) { const record = records[j]; const count = scales.length; for (let i = 0; i < count; i++) { const scale = scales[i]; const field = scale.field; record[field] = record.origin[field]; } } } if (adjust.type === 'dodge') { for (let i = 0, len = groupedArray.length; i < len; i++) { // 如果是dodge, 需要处理数字再处理 this._numberic(groupedArray[i]); } } const adjustData = adjust.adjust.process(groupedArray); // process 返回的是新数组,所以要修改 records records.forEach((record, index: number) => { record.children = adjustData[index]; adjust.adjust.setIndexMap({ index, key: record.key }); }); return adjustData; } _processData() { const { props } = this; const { data: originData } = props; const data = this._saveOrigin(originData); // 根据分类度量进行数据分组 const records = this._groupData(data); this._createAdjust(); // 根据adjust分组 const dataArray = this._adjustData(records); this.dataArray = dataArray; // scale适配调整,主要是调整 y 轴是否从 0 开始 以及 饼图 this._adjustScales(); // 数据排序(非必须) if (this.sortable) { this._sortData(records); } this.dataRecords = records; } _readjustData(records) { const { adjust } = this; if (!adjust) return; // 根据adjust分组 const dataArray = this._adjustData(records); this.dataArray = dataArray; } _sortData(records) { const xScale = this.getXScale(); const { field, type } = xScale; if (type !== 'identity' && xScale.values.length > 1) { each(records, ({ children }) => { children.sort((record1, record2) => { if (type === 'timeCat') { return ( toTimeStamp(record1[FIELD_ORIGIN][field]) - toTimeStamp(record2[FIELD_ORIGIN][field]) ); } const normalized1 = xScale.translate(record1[FIELD_ORIGIN][field]); const normalized2 = xScale.translate(record2[FIELD_ORIGIN][field]); if (isNaN(normalized1)) { return 1; } if (isNaN(normalized2)) { return -1; } return normalized1 - normalized2; }); }); } } getY0Value() { const { attrs, props } = this; const { chart } = props; const { field } = attrs.y; const scale = chart.getScale(field); return chart.scale.getZeroValue(scale); } // 根据各属性映射的值域来获取真正的绘图属性 _getShapeStyle(shape, origin) { const { context, props, geomType } = this; const { theme } = context; const shapeTheme = theme.shape[geomType] || {}; const defaultShapeStyle = shapeTheme.default; const shapeThemeStyle = shapeTheme[shape]; const { style } = props; const shapeStyle = { ...defaultShapeStyle, ...shapeThemeStyle, }; if (!style || !isObject(style)) { return shapeStyle; } // @ts-ignore const { field, ...styles } = style; const value = field ? origin[field] : origin; each(styles, (attr, key) => { if (isFunction(attr)) { const attrValue = attr(value); if (!attrValue) { return; } shapeStyle[key] = attrValue; return; } shapeStyle[key] = attr; }); return shapeStyle; } /** * 数据映射到视图属性核心逻辑 * x、y 每个元素走 normalize 然后 convertPoint * color、size、shape * 如果是Linear,则每个元素 走 mapping * 如果是Category/Identity 则第一个元素走 mapping */ _mapping(records) { const { attrs, props, attrController } = this; const { coord } = props; const { linearAttrs, nonlinearAttrs } = attrController.getAttrsByLinear(); const defaultAttrValues = attrController.getDefaultAttrValues(); const mappedRecords = []; for (let i = 0, len = records.length; i < len; i++) { const record = records[i]; const { children } = record; const attrValues = { ...defaultAttrValues, }; const firstChild = children[0]; if (children.length === 0) { mappedRecords.push({ ...record }); continue; } // 非线性映射 for (let k = 0, len = nonlinearAttrs.length; k < len; k++) { const attrName = nonlinearAttrs[k]; const attr = attrs[attrName]; // 非线性映射只用映射第一项就可以了 attrValues[attrName] = attr.mapping(firstChild[attr.field], firstChild.origin); } // 线性属性映射 const mappedChildren = []; for (let j = 0, childrenLen = children.length; j < childrenLen; j++) { const child = children[j]; const normalized: any = {}; for (let k = 0; k < linearAttrs.length; k++) { const attrName = linearAttrs[k]; const attr = attrs[attrName]; const value = child[attr.field]; // 分类属性的线性映射 if (attrController.isGroupAttr(attrName)) { attrValues[attrName] = attr.mapping(value, child); } else { normalized[attrName] = attr.normalize(value); } } const { x, y } = coord.convertPoint({ x: normalized.x, y: normalized.y, }); // 获取 shape 的 style const { origin } = child; const shapeName = attrValues.shape; const shape = this._getShapeStyle(shapeName, origin); const selected = this.isSelected(child); // 饼图 Interval 的 y 值设置为 color, normalized, x, y, shapeName, shape, selected 等字段时, // 会导致取值异常 mappedChildren.push({ ...child, ...attrValues, normalized, x, y, shapeName, shape, selected, }); } mappedRecords.push({ ...record, children: mappedChildren, }); } return mappedRecords; } // 数据映射 mapping() { const { dataRecords } = this; // 数据映射 this.records = this._mapping(dataRecords); return this.records; } getClip() { const { coord, viewClip } = this.props; const { width: contentWidth, height: contentHeight, left, top } = coord; if (viewClip) { return { type: 'rect', style: { x: left, y: top, width: contentWidth, height: contentHeight, }, }; } return null; } getAttr(attrName: string) { return this.attrController.getAttr(attrName); } getXScale(): Scale { return this.getAttr('x').scale; } getYScale(): Scale { return this.getAttr('y').scale; } getColorScale(): Scale { return this.getAttr('color').scale; } _getXSnap(invertPointX) { const xScale = this.getXScale(); if (xScale.isCategory) { return xScale.invert(invertPointX); } // linear 类型 const invertValue = xScale.invert(invertPointX); const values = xScale.values; const len = values.length; // 如果只有1个点直接返回第1个点 if (len === 1) { return values[0]; } // 第1个点和第2个点之间 if ((values[0] + values[1]) / 2 > invertValue) { return values[0]; } // 最后2个点 if ((values[len - 2] + values[len - 1]) / 2 <= invertValue) { return values[len - 1]; } for (let i = 1; i < len; i++) { // 中间的点 if ( (values[i - 1] + values[i]) / 2 <= invertValue && (values[i + 1] + values[i]) / 2 > invertValue ) { return values[i]; } } return null; } _getYSnapRecords(invertPointY, records) { const yScale = this.getYScale(); const { field: yField } = yScale; const yValue = yScale.invert(invertPointY); // category if (yScale.isCategory) { return records.filter((record) => record[FIELD_ORIGIN][yField] === yValue); } // 与 `this._mapping` 返回的中的 item 一一对应 const originRecordList = flatten(this.dataRecords.map((r) => r.children)); // linear return records.filter((record, idx) => { const isOverrideField = OVERRIDE_FIELDS_SET.has(yField); const rangeY = isOverrideField ? originRecordList[idx]?.[yField] : record[yField]; if (rangeY?.[0] <= yValue && rangeY?.[1] >= yValue) { return true; } return false; }); } _getXSnapRecords(invertPointX, records) { const xScale = this.getXScale(); const { field: xField } = xScale; const xValue = xScale.invert(invertPointX); // category if (xScale.isCategory) { return records.filter((record) => record[FIELD_ORIGIN][xField] === xValue); } // linear return records.filter((record) => { const rangeX = record[xField]; if (rangeX[0] <= xValue && rangeX[1] >= xValue) { return true; } return false; }); } // 把 records 拍平 flatRecords() { const { records } = this; return records.reduce((prevRecords, record) => { return prevRecords.concat(record.children); }, []); } getSnapRecords(point, inCoordRange?): any[] { const { props } = this; const { coord, adjust } = props; const invertPoint = coord.invertPoint(point); const xScale = this.getXScale(); const yScale = this.getYScale(); // 如果不在coord坐标范围内,直接返回空 // if (invertPoint.x < 0 || invertPoint.y < 0) { // return []; // } // 是否调整 point,默认为不调整 if (inCoordRange) { const { range: xRange } = xScale; const { range: yRange } = yScale; // 如果 inCoordRange=true,当 point 不在 coord 坐标范围内时,调整到 range 内 invertPoint.x = Math.min(Math.max(invertPoint.x, xRange[0]), xRange[1]); invertPoint.y = Math.min(Math.max(invertPoint.y, yRange[0]), yRange[1]); } const records = this.flatRecords(); const xValue = xScale.invert(invertPoint.x); const yValue = yScale.invert(invertPoint.y); const coordPoint = coord.convertPoint(invertPoint); const coordRecord = { // 坐标点 x: coordPoint.x, y: coordPoint.y, xValue, yValue, xText: xScale.getText(xValue), yText: yScale.getText(yValue), }; // 处理饼图 if (adjust === 'stack' && coord.isPolar) { if (coord.transposed) { // 弧度在半径范围内 if (invertPoint.x >= 0 && invertPoint.x <= 1) { const snapRecords = this._getYSnapRecords(invertPoint.y, records); return snapRecords; } } else { if (invertPoint.y >= 0 && invertPoint.y <= 1) { const snapRecords = this._getXSnapRecords(invertPoint.x, records); return snapRecords; } } } const rst = []; const value = this._getXSnap(invertPoint.x); if (isNull(value)) { return rst; } const { field: xField } = xScale; const { field: yField } = yScale; for (let i = 0, len = records.length; i < len; i++) { const record = { ...records[i], xField, yField, coord: coordRecord, }; const originValue = record[FIELD_ORIGIN][xField]; if (xScale.type === 'timeCat' && toTimeStamp(originValue) === value) { rst.push(record); } else if (originValue === value) { rst.push(record); } } return rst; } getRecords(data, field = 'xfield') { const records = this.flatRecords(); const xScale = this.getXScale(); const yScale = this.getYScale(); const { field: xField } = xScale; const { field: yField } = yScale; const value = data[xField]; const rst = []; for (let i = 0, len = records.length; i < len; i++) { const record = { ...records[i], xField, yField, }; const originValue = record[FIELD_ORIGIN][field === 'xfield' ? xField : yField]; if (originValue === value) { rst.push(record); } } return rst; } getLegendItems() { const { attrController, records } = this; const colorAttr = attrController.getAttr('color'); if (!colorAttr) return null; const { scale } = colorAttr; const { isCategory, field } = scale; if (!isCategory) return null; const flatRecords = records ? this.flatRecords() : []; const ticks = scale.getTicks(); const items = ticks.map((tick) => { const { text, tickValue } = tick; const record = find(flatRecords, (item) => { if (!item) return false; const { origin } = item; return origin[field] === tickValue; }); // @ts-ignore const color = record ? record.color : colorAttr.mapping(tickValue); return { field: scale.field, color, name: text, // for display tickValue, }; }); return items; } } export default Geometry;