@antv/g2
Version:
the Grammar of Graphics in Javascript
1,926 lines (1,775 loc) • 63.3 kB
text/typescript
import { Adjust, getAdjust as getAdjustClass } from '@antv/adjust';
import { Attribute, getAttribute as getAttributeClass } from '@antv/attr';
import {
clone,
deepMix,
each,
flatten,
get,
isArray,
isEmpty,
isEqual,
isFunction,
isNil,
isNumber,
isObject,
isPlainObject,
isString,
set,
} from '@antv/util';
import { doGroupAppearAnimate, getDefaultAnimateCfg } from '../animate';
import Base from '../base';
import { FIELD_ORIGIN, GEOMETRY_LIFE_CIRCLE, GROUP_ATTRS } from '../constant';
import { BBox, Coordinate, IGroup, IShape, Scale } from '../dependents';
import {
AdjustOption,
AdjustType,
AnimateOption,
AttributeOption,
ColorAttrCallback,
Data,
Datum,
GeometryLabelCfg,
GeometryTooltipOption,
LabelCallback,
LabelOption,
LooseObject,
MappingDatum,
ScaleOption,
ShapeAttrCallback,
ShapeFactory,
ShapeInfo,
ShapeMarkerCfg,
ShapeMarkerAttrs,
ShapePoint,
SizeAttrCallback,
StateOption,
StyleCallback,
StyleOption,
TooltipCallback,
CustomOption,
} from '../interface';
import { uniq } from '../util/helper';
import Element from './element';
import { getGeometryLabel } from './label';
import GeometryLabel from './label/base';
import { getShapeFactory } from './shape/base';
import { group } from './util/group-data';
import { isModelChange } from './util/is-model-change';
import { parseFields } from './util/parse-fields';
import { diff } from './util/diff';
import { inferScaleType } from '../util/scale';
import { getXDimensionLength } from '../util/coordinate';
/** @ignore */
interface AttributeInstanceCfg {
fields?: string[];
callback?: (...args) => any;
values?: string[] | number[];
scales?: Scale[];
}
interface DimValuesMapType {
[dim: string]: number[];
}
/** @ignore */
interface AdjustInstanceCfg {
type: AdjustType;
adjustNames?: string[];
xField?: string;
yField?: string;
dodgeBy?: string;
marginRatio?: number;
dodgeRatio?: number;
size?: number;
height?: number;
reverseOrder?: boolean;
/** 像素级柱间宽度,调整offset */
intervalPadding?: number;
dodgePadding?: number;
/** x维度长度,计算归一化padding使用 */
xDimensionLength?: number;
/** 分组数,计算offset */
groupNum?: number;
/** 用户配置宽度 size */
defaultSize?: number;
/** 最大最小宽度约束 */
maxColumnWidth?: number;
minColumnWidth?: number;
/** 柱宽比例 */
columnWidthRatio?: number;
/** 用户自定义的dimValuesMap */
dimValuesMap?: DimValuesMapType;
}
/** geometry.init() 传入参数 */
export interface InitCfg {
/** 坐标系 */
coordinate?: Coordinate;
/** 数据 */
data?: Data;
/** 主题对象 */
theme?: LooseObject;
/** 列定义 */
scaleDefs?: Record<string, ScaleOption>;
/** 因为数据使用的引用,所以需要有一个标识位标识数据是否发生了更新 */
isDataChanged?: boolean;
isCoordinateChanged?: boolean;
}
/** Geometry 构造函数参数 */
export interface GeometryCfg {
/** Geometry shape 的容器。 */
container: IGroup;
/** 绘制的坐标系对象。 */
coordinate?: Coordinate;
/** 绘制数据。 */
data?: Data;
/** 需要的 scales。 */
scales?: Record<string, Scale>;
/** 列定义 */
scaleDefs?: Record<string, ScaleOption>;
/** Geometry labels 的容器 */
labelsContainer?: IGroup;
/** 是否对数据进行排序 */
sortable?: boolean;
/** elements 的 zIndex 默认按顺序提升,通过 zIndexReversed 可以反序,从而数据越前,层级越高 */
zIndexReversed?: boolean;
/** 是否需要对 zIndex 进行 sort。因为耗时长,由具体场景自行决定 */
sortZIndex?: boolean;
/** 延迟渲染 Geometry 数据标签. 设置为 true 时,会在浏览器空闲时期被调用, 也可以指定具体 timeout 时间 */
useDeferredLabel?: boolean | number;
/** 是否可见 */
visible?: boolean;
/** 主题配置 */
theme?: LooseObject;
/** 组间距 */
intervalPadding?: number;
/** 组内间距 */
dodgePadding?: number;
/** 柱状图最大宽度 */
maxColumnWidth?: number;
/** 柱状图最小宽度 */
minColumnWidth?: number;
/** 默认宽度占比,interval类型和schema类型通用 */
columnWidthRatio?: number;
/** 玫瑰图占比 */
roseWidthRatio?: number;
/** 多层饼图/环图占比 */
multiplePieWidthRatio?: number;
}
/**
* Geometry 几何标记基类,主要负责数据到图形属性的映射以及绘制逻辑。
*/
export default class Geometry<S extends ShapePoint = ShapePoint> extends Base {
/** Geometry 几何标记类型。 */
public readonly type: string = 'base';
/** ShapeFactory 对应的类型。 */
public readonly shapeType: string;
// 在创建 Geometry 实例时可以传入的属性
/** Coordinate 坐标系实例。 */
public coordinate: Coordinate;
/** 用户绘制数据。 */
public data: Data;
/** 图形绘制容器。 */
public readonly container: IGroup;
/** label 绘制容器。 */
public readonly labelsContainer: IGroup;
/** 是否对数据进行排序,默认为 false。 */
public sortable: boolean;
/** 当前 Geometry 实例主题。 */
public theme: LooseObject;
/** 存储 geometry 需要的 scales,需要外部传入。 */
public scales: Record<string, Scale>;
/** scale 定义,需要外部传入。 */
public scaleDefs: Record<string, ScaleOption>;
/** 画布区域,用于 label 布局。 */
public canvasRegion: BBox;
// 内部产生的属性
/** Attribute map */
public attributes: Record<string, Attribute> = {};
/** Element map */
public elements: Element[] = [];
/**
* 存储处理后的数据,
* + init() 及 updateData() 逻辑后, 结构为 Data[];
* + paint() 逻辑后,结构为 MappingDatum[][]。
*/
public dataArray: MappingDatum[][];
/** 存储 tooltip 配置信息。 */
public tooltipOption: GeometryTooltipOption | boolean;
/** 存储 label 配置信息。 */
public labelOption: LabelOption | false;
/** 状态量相关的配置项 */
public stateOption: StateOption;
/** 使用 key-value 结构存储 Element,key 为每个 Element 实例对应的唯一 ID */
public elementsMap: Record<string, Element> = {};
/** animate 配置项 */
public animateOption: AnimateOption | boolean = true;
/** 图形属性映射配置 */
protected attributeOption: Record<string, AttributeOption> = {};
/** adjust 配置项 */
protected adjustOption: AdjustOption[];
/** style 配置项 */
protected styleOption: StyleOption;
/** custom 自定义的配置项 */
protected customOption: CustomOption;
/** 每个 Geometry 对应的 Shape 工厂实例,用于创建各个 Shape */
protected shapeFactory: ShapeFactory;
/** 存储上一次渲染时的 element 映射表,用于更新逻辑 */
protected lastElementsMap: Record<string, Element> = {};
/** 是否生成多个点来绘制图形。 */
protected generatePoints: boolean = false;
/** 存储发生图形属性映射前的数据 */
protected beforeMappingData: Data[] = null;
/** 存储每个 shape 的默认 size,用于 Interval、Schema 几何标记 */
protected defaultSize: number;
// 用户通过 geometry 构造函数设置的主题
private userTheme: LooseObject;
private adjusts: Record<string, Adjust> = {};
private lastAttributeOption;
private idFields: string[] = [];
private geometryLabel: GeometryLabel;
// 柱状图间距相关配置
/** 组间距 */
protected intervalPadding: number;
/** 组内间距 */
protected dodgePadding: number;
/** 柱状图最大宽度 */
protected maxColumnWidth: number;
/** 柱状图最小宽度 */
protected minColumnWidth: number;
/** 一般柱状图宽度占比 */
protected columnWidthRatio: number;
/** 玫瑰图占比 */
protected roseWidthRatio: number;
/** 多层饼图/环图占比 */
protected multiplePieWidthRatio: number;
/** elements 的 zIndex 默认按顺序提升,通过 zIndexReversed 可以反序,从而数据越前,层级越高 */
public zIndexReversed?: boolean;
/** 是否需要对 zIndex 进行 sort。因为耗时长,由具体场景自行决定 */
public sortZIndex?: boolean;
protected useDeferredLabel?: null | number;
/** 虚拟 Group,用于图形更新 */
private offscreenGroup: IGroup;
private groupScales: Scale[];
private hasSorted: boolean = false;
protected isCoordinateChanged: boolean = false;
/**
* 创建 Geometry 实例。
* @param cfg
*/
constructor(cfg: GeometryCfg) {
super(cfg);
const {
container,
labelsContainer,
coordinate,
data,
sortable = false,
visible = true,
theme,
scales = {},
scaleDefs = {},
// 柱状图间隔与宽度相关配置
intervalPadding,
dodgePadding,
maxColumnWidth,
minColumnWidth,
columnWidthRatio,
roseWidthRatio,
multiplePieWidthRatio,
zIndexReversed,
sortZIndex,
useDeferredLabel,
} = cfg;
this.container = container;
this.labelsContainer = labelsContainer;
this.coordinate = coordinate;
this.data = data;
this.sortable = sortable;
this.visible = visible;
this.userTheme = theme;
this.scales = scales;
this.scaleDefs = scaleDefs;
// 柱状图间隔与宽度相关配置
this.intervalPadding = intervalPadding;
this.dodgePadding = dodgePadding;
this.maxColumnWidth = maxColumnWidth;
this.minColumnWidth = minColumnWidth;
this.columnWidthRatio = columnWidthRatio;
this.roseWidthRatio = roseWidthRatio;
this.multiplePieWidthRatio = multiplePieWidthRatio;
this.zIndexReversed = zIndexReversed;
this.sortZIndex = sortZIndex;
this.useDeferredLabel = useDeferredLabel ? (typeof useDeferredLabel === 'number' ? useDeferredLabel : Infinity) : null;
}
/**
* 配置 position 通道映射规则。
*
* @example
* ```typescript
* // 数据结构: [{ x: 'A', y: 10, color: 'red' }]
* geometry.position('x*y');
* geometry.position([ 'x', 'y' ]);
* geometry.position({
* fields: [ 'x', 'y' ],
* });
* ```
*
* @param cfg 映射规则
* @returns
*/
public position(cfg: string | string[] | AttributeOption): Geometry {
let positionCfg = cfg;
if (!isPlainObject(cfg)) {
// 字符串字段或者数组字段
positionCfg = {
fields: parseFields(cfg),
};
}
const fields = get(positionCfg, 'fields');
if (fields.length === 1) {
// 默认填充一维 1*xx
fields.unshift('1');
set(positionCfg, 'fields', fields);
}
set(this.attributeOption, 'position', positionCfg);
return this;
}
/**
* 配置 color 通道映射规则。
*
* @example
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
* geometry.color({
* fields: [ 'x' ],
* values: [ '#1890ff', '#5AD8A6' ],
* });
* ```
*
* @param field 映射规则
* @returns
*/
public color(field: AttributeOption): Geometry;
/**
* @example
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
*
* // 使用 '#1890ff' 颜色渲染图形
* geometry.color('#1890ff');
*
* // 根据 x 字段的数据值进行颜色的映射,这时候 G2 会在内部调用默认的回调函数,读取默认提供的颜色进行数据值到颜色值的映射。
* geometry.color('x');
*
* // 将 'x' 字段的数据值映射至指定的颜色值 colors(可以是字符串也可以是数组),此时用于通常映射分类数据
* geometry.color('x', [ '#1890ff', '#5AD8A6' ]);
*
* // 使用回调函数进行颜色值的自定义;可以使用多个字段使用、*号连接
* geometry.color('x', (xVal) => {
* if (xVal === 'a') {
* return 'red';
* }
* return 'blue';
* });
*
* // 指定颜色的渐变路径,用于映射连续的数据
* geometry.color('x', '#BAE7FF-#1890FF-#0050B3');
* ```
*
* @param field 参与颜色映射的数据字段,多个字段使用 '*' 连接符进行连接。
* @param cfg Optional, color 映射规则。
* @returns
*/
public color(field: string, cfg?: string | string[] | ColorAttrCallback): Geometry;
public color(field: AttributeOption | string, cfg?: string | string[] | ColorAttrCallback): Geometry {
this.createAttrOption('color', field, cfg);
return this;
}
/**
* 配置 shape 通道映射规则。
*
* @example
*
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
* geometry.shape({
* fields: [ 'x' ],
* });
* ```
*
* @param field 映射规则配置。
* @returns
*/
public shape(field: AttributeOption): Geometry;
/**
*
* @example
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
*
* // 指定常量,将所有数据值映射到固定的 shape
* geometry.shape('circle');
*
* // 将指定的字段映射到内置的 shapes 数组中
* geometry.shape('x');
*
* // 将指定的字段映射到指定的 shapes 数组中
* geometry.shape('x', [ 'circle', 'diamond', 'square' ]);
*
* // 使用回调函数获取 shape,用于个性化的 shape 定制,可以根据单个或者多个字段确定
* geometry.shape('x', (xVal) => {
* if (xVal === 'a') {
* return 'circle';
* }
* return 'diamond';
* });
* ```
*
* @param field 参与 shape 映射的数据字段,多个字段使用 '*' 连接符进行连接。
* @param cfg Optional, shape 映射规则。
* @returns
*/
public shape(field: string, cfg?: string[] | ShapeAttrCallback): Geometry;
public shape(field: AttributeOption | string, cfg?: string[] | ShapeAttrCallback): Geometry {
this.createAttrOption('shape', field, cfg);
return this;
}
/**
* 配置 size 通道映射规则。
*
* @example
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
* geometry.size({
* values: [ 10 ],
* })
* ```
*
* @param field 映射规则。
* @returns
*/
public size(field: AttributeOption): Geometry;
/**
*
* @example
* ```typescript
* // data: [{ x: 'A', y: 10, color: 'red' }, { x: 'B', y: 30, color: 'yellow' }]
*
* // 直接指定像素大小
* geometry.size(10);
*
* // 指定映射到 size 的字段,使用内置的默认大小范围为 [1, 10]
* geometry.size('x');
*
* // 指定映射到 size 字段外,还提供了 size 的最大值和最小值范围
* geometry.size('x', [ 5, 30 ]);
*
* // 使用回调函数映射 size,用于个性化的 size 定制,可以使用多个字段进行映射
* geometry.size('x', (xVal) => {
* if (xVal === 'a') {
* return 10;
* }
* return 5;
* });
* ```
*
* @param field 参与 size 映射的数据字段,多个字段使用 '*' 连接符进行连接。
* @param cfg Optional, size 映射规则
* @returns
*/
public size(field: number | string, cfg?: [number, number] | SizeAttrCallback): Geometry;
public size(field: AttributeOption | number | string, cfg?: [number, number] | SizeAttrCallback): Geometry {
this.createAttrOption('size', field, cfg);
return this;
}
/**
* 设置数据调整方式。G2 目前内置了四种类型:
* 1. dodge
* 2. stack
* 3. symmetric
* 4. jitter
*
*
* **Tip**
* + 对于 'dodge' 类型,可以额外进行如下属性的配置:
* ```typescript
* geometry.adjust('dodge', {
* marginRatio: 0, // 取 0 到 1 范围的值(相对于每个柱子宽度),用于控制一个分组中柱子之间的间距
* dodgeBy: 'x', // 该属性只对 'dodge' 类型生效,声明以哪个数据字段为分组依据
* });
* ```
*
* + 对于 'stack' 类型,可以额外进行如下属性的配置:
* ```typescript
* geometry.adjust('stack', {
* reverseOrder: false, // 用于控制是否对数据进行反序操作
* });
* ```
*
* @example
* ```typescript
* geometry.adjust('stack');
*
* geometry.adjust({
* type: 'stack',
* reverseOrder: false,
* });
*
* // 组合使用 adjust
* geometry.adjust([ 'stack', 'dodge' ]);
*
* geometry.adjust([
* { type: 'stack' },
* { type: 'dodge', dodgeBy: 'x' },
* ]);
* ```
*
* @param adjustCfg 数据调整配置
* @returns
*/
public adjust(adjustCfg: string | string[] | AdjustOption | AdjustOption[]): Geometry {
let adjusts: any = adjustCfg;
if (isString(adjustCfg) || isPlainObject(adjustCfg)) {
adjusts = [adjustCfg];
}
each(adjusts, (adjust, index) => {
if (!isObject(adjust)) {
adjusts[index] = { type: adjust };
}
});
this.adjustOption = adjusts;
return this;
}
/**
* 图形样式配置。
*
* @example
* ```typescript
* // 配置图形样式
* style({
* lineWidth: 2,
* stroke: '#1890ff',
* });
*
* // 根据具体的数据进行详细配置
* style({
* fields: [ 'x', 'y' ], // 数据字段
* callback: (xVal, yVal) => {
* const style = { lineWidth: 2, stroke: '#1890ff' };
* if (xVal === 'a') {
* style.lineDash = [ 2, 2 ];
* }
* return style;
* },
* });
* ```
*
* @param field 配置样式属性或者样式规则。
* @returns
*/
public style(field: StyleOption | LooseObject): Geometry;
/**
* @example
* ```typescript
* style('x*y', (xVal, yVal) => {
* const style = { lineWidth: 2, stroke: '#1890ff' };
* if (xVal === 'a') {
* style.lineDash = [ 2, 2 ];
* }
* return style;
* });
* ```
*
* @param field 数据字段或者样式配置规则。
* @param styleFunc Optional, 样式配置回调函数。
* @returns
*/
public style(field: string, styleFunc: StyleCallback): Geometry;
public style(field: StyleOption | LooseObject | string, styleFunc?: StyleCallback): Geometry {
if (isString(field)) {
const fields = parseFields(field);
this.styleOption = {
fields,
callback: styleFunc,
};
} else {
const { fields, callback, cfg } = field as StyleOption;
if (fields || callback || cfg) {
this.styleOption = field;
} else {
this.styleOption = {
cfg: field,
};
}
}
return this;
}
/**
* 配置 Geometry 显示的 tooltip 内容。
*
* `tooltip(false)` 代表关闭 tooltip。
* `tooltip(true)` 代表开启 tooltip。
*
* Geometry 默认允许 tooltip 展示,我们可以使用以下方法对 tooltip 的展示内容进行配置:
*
* @example
* ```typescript
* // data: [{x: 'a', y: 10}]
* tooltip({
* fields: [ 'x' ],
* });
* ```
* 
*
* ```typescript
* tooltip({
* fields: [ 'x', 'y' ],
* });
* ```
* 
*
* tooltip() 方法同样支持数据映射及回调用法:
*
* @example
* ```typescript
* chart.tooltip({
* itemTpl: '<li>{x}: {y}</li>',
* });
*
* chart.line()
* .position('x*y')
* .tooltip({
* fields: [ 'x', 'y' ],
* callback: (x, y) => {
* return {
* x,
* y,
* };
* },
* });
* ```
*
* 其返回的值必须为对象,该值中的属性同 chart.tooltip() 的 itemTpl 模板相对应,返回的变量可用于 itemTpl 的字符串模板。
*
* @param field tooltip 配置信息。
* @returns
*/
public tooltip(field: GeometryTooltipOption | boolean): Geometry;
/**
* @example
* ```typescript
* // data: [{x: 'a', y: 10}]
*
* // 等同于 tooltip({ fields: [ 'x' ] })
* tooltip('x');
*
* // 等同于 tooltip({ fields: [ 'x', 'y' ] })
* tooltip('x*y');
*
* // 等同于 tooltip({ fields: [ 'x', 'y' ], callback: (x, y) => { x, y } })
* tooltip('x*y', (x, y) => {
* return {
* x,
* y,
* };
* });
* ```
*
* @param field 参与映射的字段。
* @param cfg Optional, 回调函数
* @returns
*/
public tooltip(field: string, cfg?: TooltipCallback): Geometry;
public tooltip(field: GeometryTooltipOption | boolean | string, cfg?: TooltipCallback): Geometry {
if (isString(field)) {
const fields = parseFields(field);
this.tooltipOption = {
fields,
callback: cfg,
};
} else {
this.tooltipOption = field;
}
return this;
}
/**
* Geometry 动画配置。
*
* + `animate(false)` 关闭动画
* + `animate(true)` 开启动画,默认开启。
*
* 我们将动画分为四个场景:
* 1. appear: 图表第一次加载时的入场动画;
* 2. enter: 图表绘制完成,发生更新后,产生的新图形的进场动画;
* 3. update: 图表绘制完成,数据发生变更后,有状态变更的图形的更新动画;
* 4. leave: 图表绘制完成,数据发生变更后,被销毁图形的销毁动画。
*
* @example
* ```typescript
* animate({
* enter: {
* duration: 1000, // enter 动画执行时间
* },
* leave: false, // 关闭 leave 销毁动画
* });
* ```
*
* @param cfg 动画配置
* @returns
*/
public animate(cfg: AnimateOption | boolean): Geometry {
this.animateOption = cfg;
return this;
}
/**
* Geometry label 配置。
*
* @example
* ```ts
* // data: [ {x: 1, y: 2, z: 'a'}, {x: 2, y: 2, z: 'b'} ]
* // 在每个图形上显示 z 字段对应的数值
* label({
* fields: [ 'z' ]
* });
*
* label(false); // 不展示 label
*
* // 在每个图形上显示 x 字段对应的数值,同时配置文本颜色为红色
* label('x', {
* style: {
* fill: 'red',
* },
* })
*
* // 以 type 类型的 label 渲染每个图形上显示 x 字段对应的数值,同时格式化文本内容
* label('x', (xValue) => {
* return {
* content: xValue + '%',
* };
* }, {
* type: 'base' // 声明 label 类型
* })
* ```
*
* @param field
* @returns label
*/
public label(field: LabelOption | false | string): Geometry;
public label(field: string, secondParam: GeometryLabelCfg | LabelCallback): Geometry;
public label(field: string, secondParam: LabelCallback, thirdParam: GeometryLabelCfg): Geometry;
public label(
field: string | LabelOption | false,
secondParam?: GeometryLabelCfg | LabelCallback,
thirdParam?: GeometryLabelCfg
): Geometry {
if (isString(field)) {
const labelOption: LabelOption = {};
const fields = parseFields(field);
labelOption.fields = fields;
if (isFunction(secondParam)) {
labelOption.callback = secondParam;
} else if (isPlainObject(secondParam)) {
labelOption.cfg = secondParam;
}
if (thirdParam) {
labelOption.cfg = thirdParam;
}
this.labelOption = labelOption;
} else {
this.labelOption = field;
}
return this;
}
/**
* 设置状态对应的样式。
*
* @example
* ```ts
* chart.interval().state({
* selected: {
* animate: { duration: 100, easing: 'easeLinear' },
* style: {
* lineWidth: 2,
* stroke: '#000',
* },
* },
* });
* ```
*
* 如果图形 shape 是由多个 shape 组成,即为一个 G.Group 对象,那么针对 group 中的每个 shape,我们需要使用下列方式进行状态样式设置:
* 如果我们为 group 中的每个 shape 设置了 'name' 属性(shape.set('name', 'xx')),则以 'name' 作为 key,否则默认以索引值(即 shape 的 添加顺序)为 key。
*
* ```ts
* chart.interval().shape('groupShape').state({
* selected: {
* style: {
* 0: { lineWidth: 2 },
* 1: { fillOpacity: 1 },
* }
* }
* });
* ```
*
* @param cfg 状态样式
*/
public state(cfg: StateOption) {
this.stateOption = cfg;
return this;
}
/**
* 用于向 shape 中传入自定义的数据。目前可能仅仅可能用于在自定义 shape 的时候,像自定义 shape 中传入自定义的数据,方便实现自定义 shape 的配置能力。
*
* @example
* ```ts
* chart.interval().customInfo({ yourData: 'hello, g2!' });
* ```
*
* 然后在自定义 shape 的时候,可以拿到这个信息。
*
* ```ts
* registerShape('interval', 'your-shape', {
* draw(shapeInfo, container) {
* const { customInfo } = shapeInfo;
* console.log(customInfo); // will log { yourData: 'hello, g2!' }.
* }
* });
* ```
*
* @param cfg
*/
public customInfo(cfg: any) {
this.customOption = cfg;
return this;
}
/**
* 初始化 Geomtry 实例:
* 创建 [[Attribute]] and [[Scale]] 实例,进行数据处理,包括分组、数值化以及数据调整。
*/
public init(cfg: InitCfg = {}) {
this.setCfg(cfg);
this.initAttributes(); // 创建图形属性
// 数据加工:分组 -> 数字化 -> adjust
this.processData(this.data);
// 调整 scale
this.adjustScale();
}
/**
* Geometry 更新。
* @param [cfg] 更新的配置
*/
public update(cfg: InitCfg = {}) {
const { data, isDataChanged, isCoordinateChanged } = cfg;
const { attributeOption, lastAttributeOption } = this;
if (!isEqual(attributeOption, lastAttributeOption)) {
// 映射发生改变,则重新创建图形属性
this.init(cfg);
} else if (data && (isDataChanged || !isEqual(data, this.data))) {
// 数据发生变化
this.setCfg(cfg);
this.initAttributes(); // 创建图形属性
this.processData(data); // 数据加工:分组 -> 数字化 -> adjust
} else {
// 有可能 coordinate 变化
this.setCfg(cfg);
}
// 调整 scale
this.adjustScale();
this.isCoordinateChanged = isCoordinateChanged;
}
/**
* 将原始数据映射至图形空间,同时创建图形对象。
*/
public paint(isUpdate: boolean = false) {
if (this.animateOption) {
this.animateOption = deepMix({}, getDefaultAnimateCfg(this.type, this.coordinate), this.animateOption);
}
this.defaultSize = undefined;
this.elementsMap = {};
this.elements = [];
const offscreenGroup = this.getOffscreenGroup();
offscreenGroup.clear();
const beforeMappingData = this.beforeMappingData;
const dataArray = this.beforeMapping(beforeMappingData);
this.dataArray = new Array(dataArray.length);
for (let i = 0; i < dataArray.length; i++) {
const data = dataArray[i];
this.dataArray[i] = this.mapping(data);
}
this.updateElements(this.dataArray, isUpdate);
this.lastElementsMap = this.elementsMap;
if (this.canDoGroupAnimation(isUpdate)) {
// 如果用户没有配置 appear.animation,就默认走整体动画
const container = this.container;
const type = this.type;
const coordinate = this.coordinate;
const animateCfg = get(this.animateOption, 'appear');
const yScale = this.getYScale();
const yMinPoint = coordinate.convert({
x: 0,
y: yScale.scale(this.getYMinValue()),
});
doGroupAppearAnimate(container, animateCfg, type, coordinate, yMinPoint);
}
// 添加 label
if (this.labelOption) {
const deferred = this.useDeferredLabel;
const callback = (() => this.renderLabels(flatten(this.dataArray) as unknown as MappingDatum[], isUpdate)).bind(this);
if (typeof deferred === 'number') {
// Use `requestIdleCallback` to render labels in idle time (like react fiber)
const timeout = (typeof deferred === 'number' && deferred !== Infinity) ? deferred : 0;
if (!window.requestIdleCallback) {
setTimeout(callback, timeout);
} else {
const options = timeout && timeout !== Infinity ? { timeout } : undefined;
window.requestIdleCallback(callback, options);
}
} else {
callback();
}
}
// 缓存,用于更新
this.lastAttributeOption = {
...this.attributeOption,
};
if (this.visible === false) {
// 用户在初始化的时候声明 visible: false
this.changeVisible(false);
}
}
/**
* 清空当前 Geometry,配置项仍保留,但是内部创建的对象全部清空。
* @override
*/
public clear() {
const { container, geometryLabel, offscreenGroup } = this;
if (container) {
container.clear();
}
if (geometryLabel) {
geometryLabel.clear();
}
if (offscreenGroup) {
offscreenGroup.clear();
}
// 属性恢复至出厂状态
this.scaleDefs = undefined;
this.attributes = {};
this.scales = {};
this.elementsMap = {};
this.lastElementsMap = {};
this.elements = [];
this.adjusts = {};
this.dataArray = null;
this.beforeMappingData = null;
this.lastAttributeOption = undefined;
this.defaultSize = undefined;
this.idFields = [];
this.groupScales = undefined;
this.hasSorted = false;
this.isCoordinateChanged = false;
}
/**
* 销毁 Geometry 实例。
*/
public destroy() {
this.clear();
const container = this.container;
container.remove(true);
if (this.offscreenGroup) {
this.offscreenGroup.remove(true);
this.offscreenGroup = null;
}
if (this.geometryLabel) {
this.geometryLabel.destroy();
this.geometryLabel = null;
}
this.theme = undefined;
this.shapeFactory = undefined;
super.destroy();
}
/**
* 获取决定分组的图形属性对应的 scale 实例。
* @returns
*/
public getGroupScales(): Scale[] {
return this.groupScales;
}
/**
* 根据名字获取图形属性实例。
*/
public getAttribute(name: string): Attribute {
return this.attributes[name];
}
/** 获取 x 轴对应的 scale 实例。 */
public getXScale(): Scale {
return this.getAttribute('position').scales[0];
}
/** 获取 y 轴对应的 scale 实例。 */
public getYScale(): Scale {
return this.getAttribute('position').scales[1];
}
/**
* 获取决定分组的图形属性实例。
*/
public getGroupAttributes(): Attribute[] {
const rst = [];
each(this.attributes, (attr: Attribute) => {
if (GROUP_ATTRS.includes(attr.type)) {
rst.push(attr);
}
});
return rst;
}
/** 获取图形属性默认的映射值。 */
public getDefaultValue(attrName: string) {
let value: any;
const attr = this.getAttribute(attrName);
if (attr && isEmpty(attr.scales)) {
// 获取映射至常量的值
value = attr.values[0];
}
return value;
}
/**
* 获取该数据发生图形映射后对应的 Attribute 图形空间数据。
* @param attr Attribute 图形属性实例。
* @param obj 需要进行映射的原始数据。
* @returns
*/
public getAttributeValues(attr: Attribute, obj: Datum) {
const params = [];
const scales = attr.scales;
for (let index = 0, length = scales.length; index < length; index++) {
const scale = scales[index];
const field = scale.field;
if (scale.isIdentity) {
params.push(scale.values);
} else {
params.push(obj[field]);
}
}
return attr.mapping(...params);
}
/**
* 获取对应的 adjust 实例
* @param adjustType
* @returns
*/
public getAdjust(adjustType: string) {
return this.adjusts[adjustType];
}
/**
* 获得 coordinate 实例
* @returns
*/
public getCoordinate() {
return this.coordinate;
}
public getData() {
return this.data;
}
/**
* 获取 shape 对应的 marker 样式。
* @param shapeName shape 具体名字
* @param cfg marker 信息
* @returns
*/
public getShapeMarker(shapeName: string, cfg: ShapeMarkerCfg): ShapeMarkerAttrs {
const shapeFactory = this.getShapeFactory();
return shapeFactory.getMarker(shapeName, cfg);
}
/**
* 根据一定的规则查找 Geometry 的 Elements。
*
* ```typescript
* getElementsBy((element) => {
* const data = element.getData();
*
* return data.a === 'a';
* });
* ```
*
* @param condition 定义查找规则的回调函数。
* @returns
*/
public getElementsBy(condition: (element: Element) => boolean): Element[] {
return this.elements.filter((element) => condition(element));
}
/**
* 获取 Geometry 的所有 Elements。
*
* ```typescript
* getElements();
* ```
*/
public getElements() {
return this.elements;
}
/**
* 获取数据对应的唯一 id。
* @param data Element 对应的绘制数据
* @returns
*/
public getElementId(data: MappingDatum | MappingDatum[]) {
data = isArray(data) ? data[0] : data;
const originData = data[FIELD_ORIGIN];
// 如果用户声明了使用哪些字段作为 id 值
if (this.idFields.length) {
let elementId = originData[this.idFields[0]];
for (let index = 1; index < this.idFields.length; index++) {
elementId += '-' + originData[this.idFields[index]];
}
return elementId;
}
const type = this.type;
const xScale = this.getXScale();
const yScale = this.getYScale();
const xField = xScale.field || 'x';
const yField = yScale.field || 'y';
const yVal = originData[yField];
let xVal;
if (xScale.type === 'identity') {
xVal = xScale.values[0];
} else {
xVal = originData[xField];
}
let id: string;
if (type === 'interval' || type === 'schema') {
id = `${xVal}`;
} else if (type === 'line' || type === 'area' || type === 'path') {
id = type;
} else {
id = `${xVal}-${yVal}`;
}
const groupScales = this.groupScales;
for (let index = 0, length = groupScales.length; index < length; index++) {
const groupScale = groupScales[index];
const field = groupScale.field;
id = `${id}-${originData[field]}`;
}
// 用户在进行 dodge 类型的 adjust 调整的时候设置了 dodgeBy 属性
const dodgeAdjust = this.getAdjust('dodge');
if (dodgeAdjust) {
const dodgeBy = dodgeAdjust.dodgeBy;
if (dodgeBy) {
id = `${id}-${originData[dodgeBy]}`;
}
}
if (this.getAdjust('jitter')) {
id = `${id}-${data.x}-${data.y}`;
}
return id;
}
/**
* 获取所有需要创建 scale 的字段名称。
*/
public getScaleFields(): string[] {
const fields = [];
const tmpMap = new Map();
const { attributeOption, labelOption, tooltipOption } = this;
// 获取图形属性上的 fields
for (const attributeType in attributeOption) {
if (attributeOption.hasOwnProperty(attributeType)) {
const eachOpt = attributeOption[attributeType];
if (eachOpt.fields) {
uniq(eachOpt.fields, fields, tmpMap);
} else if (eachOpt.values) {
// 考虑 size(10), shape('circle') 等场景
uniq(eachOpt.values, fields, tmpMap);
}
}
}
// 获取 label 上的字段
if (labelOption && labelOption.fields) {
uniq(labelOption.fields, fields, tmpMap);
}
// 获取 tooltip 上的字段
if (isObject(tooltipOption) && tooltipOption.fields) {
uniq(tooltipOption.fields, fields, tmpMap);
}
return fields;
}
/**
* 显示或者隐藏 geometry。
* @param visible
*/
public changeVisible(visible: boolean) {
super.changeVisible(visible);
const elements = this.elements;
for (let index = 0, length = elements.length; index < length; index++) {
const element = elements[index];
element.changeVisible(visible);
}
if (visible) {
if (this.container) {
this.container.show();
}
if (this.labelsContainer) {
this.labelsContainer.show();
}
} else {
if (this.container) {
this.container.hide();
}
if (this.labelsContainer) {
this.labelsContainer.hide();
}
}
}
/**
* 获得所有的字段
*/
public getFields() {
const uniqMap = new Map<string, boolean>();
const fields = [];
Object.values(this.attributeOption).forEach((cfg) => {
const fs = cfg?.fields || [];
fs.forEach((f) => {
if (!uniqMap.has(f)) {
fields.push(f);
}
uniqMap.set(f, true);
});
}, []);
return fields;
}
/**
* 获取当前配置中的所有分组 & 分类的字段。
* @return fields string[]
*/
public getGroupFields(): string[] {
const groupFields = [];
const tmpMap = new Map(); // 用于去重过滤
for (let index = 0, length = GROUP_ATTRS.length; index < length; index++) {
const attributeName = GROUP_ATTRS[index];
const cfg = this.attributeOption[attributeName];
if (cfg && cfg.fields) {
uniq(cfg.fields, groupFields, tmpMap);
}
}
return groupFields;
}
/**
* 获得图形的 x y 字段。
*/
public getXYFields() {
const [x, y] = this.attributeOption.position.fields;
return [x, y];
}
/**
* x 字段
* @returns
*/
public getXField(): string {
return get(this.getXYFields(), [0]);
}
/**
* y 字段
* @returns
*/
public getYField(): string {
return get(this.getXYFields(), [1]);
}
/**
* 获取该 Geometry 下所有生成的 shapes。
* @returns shapes
*/
public getShapes(): (IShape | IGroup)[] {
return this.elements.map((element: Element) => element.shape);
}
/**
* 获取虚拟 Group。
* @returns
*/
public getOffscreenGroup() {
if (!this.offscreenGroup) {
const GroupCtor = this.container.getGroupBase(); // 获取分组的构造函数
this.offscreenGroup = new GroupCtor({});
}
return this.offscreenGroup;
}
// 对数据进行排序
public sort(mappingArray: Data[]) {
if (!this.hasSorted) {
// 未发生过排序
const xScale = this.getXScale();
const xField = xScale.field;
for (let index = 0; index < mappingArray.length; index++) {
const itemArr = mappingArray[index];
itemArr.sort((obj1: Datum, obj2: Datum) => {
return xScale.translate(obj1[FIELD_ORIGIN][xField]) - xScale.translate(obj2[FIELD_ORIGIN][xField]);
});
}
}
this.hasSorted = true;
}
/**
* 调整度量范围。主要针对发生层叠以及一些特殊需求的 Geometry,比如 Interval 下的柱状图 Y 轴默认从 0 开始。
*/
protected adjustScale() {
const yScale = this.getYScale();
// 如果数据发生过 stack adjust,需要调整下 yScale 的数据范围
if (yScale && this.getAdjust('stack')) {
this.updateStackRange(yScale, this.beforeMappingData);
}
}
/**
* 获取当前 Geometry 对应的 Shape 工厂实例。
*/
protected getShapeFactory() {
const shapeType = this.shapeType;
if (!getShapeFactory(shapeType)) {
return;
}
if (!this.shapeFactory) {
this.shapeFactory = clone(getShapeFactory(shapeType)); // 防止多个 view 共享一个 shapeFactory 实例,导致 coordinate 被篡改
}
// 因为这里缓存了 shapeFactory,但是外部可能会变更 coordinate,导致无法重新设置到 shapeFactory 中
this.shapeFactory.coordinate = this.coordinate;
// theme 原因同上
this.shapeFactory.theme = this.theme.geometries[shapeType] || {};
return this.shapeFactory;
}
/**
* 获取每个 Shape 对应的关键点数据。
* @param obj 经过分组 -> 数字化 -> adjust 调整后的数据记录
* @returns
*/
protected createShapePointsCfg(obj: Datum): S {
const xScale = this.getXScale();
const yScale = this.getYScale();
const x = this.normalizeValues(obj[xScale.field], xScale);
let y; // 存在没有 y 的情况
if (yScale) {
y = this.normalizeValues(obj[yScale.field], yScale);
} else {
y = obj.y ? obj.y : 0.1;
}
return {
x,
y,
y0: yScale ? yScale.scale(this.getYMinValue()) : undefined,
} as S;
}
/**
* 创建 Element 实例。
* @param mappingDatum Element 对应的绘制数据
* @param [isUpdate] 是否处于更新阶段
* @returns element 返回创建的 Element 实例
*/
protected createElement(mappingDatum: MappingDatum, index: number, isUpdate: boolean = false): Element {
const { container } = this;
const shapeCfg = this.getDrawCfg(mappingDatum); // 获取绘制图形的配置信息
const shapeFactory = this.getShapeFactory();
const element = new Element({
shapeFactory,
container,
offscreenGroup: this.getOffscreenGroup(),
elementIndex: index,
});
element.animate = this.animateOption;
element.geometry = this;
element.draw(shapeCfg, isUpdate); // 绘制
return element;
}
/**
* 获取每条数据对应的图形绘制数据。
* @param mappingDatum 映射后的数据
* @returns draw cfg
*/
protected getDrawCfg(mappingDatum: MappingDatum): ShapeInfo {
const originData = mappingDatum[FIELD_ORIGIN]; // 原始数据
const cfg: ShapeInfo = {
mappingData: mappingDatum, // 映射后的数据
data: originData, // 原始数据
x: mappingDatum.x,
y: mappingDatum.y,
color: mappingDatum.color,
size: mappingDatum.size,
isInCircle: this.coordinate.isPolar,
customInfo: this.customOption,
};
let shapeName = mappingDatum.shape;
if (!shapeName && this.getShapeFactory()) {
shapeName = this.getShapeFactory().defaultShapeType;
}
cfg.shape = shapeName;
// 获取默认样式
const theme = this.theme.geometries[this.shapeType];
cfg.defaultStyle = get(theme, [shapeName, 'default'], {}).style;
if (!cfg.defaultStyle && this.getShapeFactory()) {
cfg.defaultStyle = this.getShapeFactory().getDefaultStyle(theme);
}
const styleOption = this.styleOption;
if (styleOption) {
cfg.style = this.getStyleCfg(styleOption, originData);
}
if (this.generatePoints) {
cfg.points = mappingDatum.points;
cfg.nextPoints = mappingDatum.nextPoints;
}
return cfg;
}
protected updateElements(mappingDataArray: MappingDatum[][], isUpdate: boolean = false): void {
const keyDatum = new Map<string, MappingDatum>();
const keys: string[] = [];
// 用来保持 diff 元素之后 added, updated 的相对顺序
const keyIndex = new Map<string, number>();
let index = 0;
// 获得更新数据所有的 keys
// 将更新的数据用 key 索引
for (let i = 0; i < mappingDataArray.length; i++) {
const mappingData = mappingDataArray[i];
for (let j = 0; j < mappingData.length; j++) {
const mappingDatum = mappingData[j];
const key = this.getElementId(mappingDatum);
const finalKey = keyDatum.has(key) ? `${key}-${i}-${j}` : key;
keys.push(finalKey);
keyDatum.set(finalKey, mappingDatum);
keyIndex.set(finalKey, index);
index++;
}
}
this.elements = new Array(index);
const { added, updated, removed } = diff(this.lastElementsMap, keys);
// 新建 element
for (const key of added) {
const mappingDatum = keyDatum.get(key);
const i = keyIndex.get(key);
const element = this.createElement(mappingDatum, i, isUpdate);
this.elements[i] = element;
this.elementsMap[key] = element;
if (element.shape) {
element.shape.set('zIndex', this.zIndexReversed ? this.elements.length - i : i);
}
}
// 更新 element
for (const key of updated) {
const element = this.lastElementsMap[key];
const mappingDatum = keyDatum.get(key);
const currentShapeCfg = this.getDrawCfg(mappingDatum);
const preShapeCfg = element.getModel();
const i = keyIndex.get(key);
if (this.isCoordinateChanged || isModelChange(currentShapeCfg, preShapeCfg)) {
element.animate = this.animateOption;
// 通过绘制数据的变更来判断是否需要更新,因为用户有可能会修改图形属性映射
element.update(currentShapeCfg); // 更新对应的 element
}
this.elements[i] = element;
this.elementsMap[key] = element;
if (element.shape) {
element.shape.set('zIndex', this.zIndexReversed ? this.elements.length - i : i);
}
}
// 全部 setZIndex 之后,再执行 sort
if (this.container) {
this.container.sort();
}
// 销毁被删除的 elements
for (const key of removed) {
const element = this.lastElementsMap[key];
// 更新动画配置,用户有可能在更新之前有对动画进行配置操作
element.animate = this.animateOption;
element.destroy();
}
}
/**
* 获取渲染的 label 类型。
*/
protected getLabelType(): string {
const { labelOption, coordinate, type } = this;
const { type: coordinateType, isTransposed } = coordinate;
let labelType = get(labelOption, ['cfg', 'type']);
if (!labelType) {
// 用户未定义,则进行默认的逻辑
if (coordinateType === 'polar') {
// 极坐标下使用通用的极坐标文本,转置则使用饼图
labelType = isTransposed ? 'pie' : 'polar';
} else if (coordinateType === 'theta') {
// theta 坐标系下使用饼图文本
labelType = 'pie';
} else if (type === 'interval' || type === 'polygon') {
labelType = 'interval';
} else {
labelType = 'base';
}
}
return labelType;
}
/**
* 获取 Y 轴上的最小值。
*/
protected getYMinValue(): number {
const yScale = this.getYScale();
const { min, max } = yScale;
let value: number;
if (min >= 0) {
value = min;
} else if (max <= 0) {
// 当值全位于负区间时,需要保证 ymin 在区域内,不可为 0
value = max;
} else {
value = 0;
}
return value;
}
// 创建图形属性相关的配置项
protected createAttrOption(attrName: string, field: AttributeOption | string | number, cfg?) {
if (isNil(field) || isObject(field)) {
if (isObject(field) && isEqual(Object.keys(field), ['values'])) {
// shape({ values: [ 'funnel' ] })
set(this.attributeOption, attrName, {
fields: field.values,
});
} else {
set(this.attributeOption, attrName, field);
}
} else {
const attrCfg: AttributeOption = {};
if (isNumber(field)) {
// size(3)
attrCfg.values = [field];
} else {
attrCfg.fields = parseFields(field);
}
if (cfg) {
if (isFunction(cfg)) {
attrCfg.callback = cfg;
} else {
attrCfg.values = cfg;
}
}
set(this.attributeOption, attrName, attrCfg);
}
}
protected initAttributes() {
const { attributes, attributeOption, theme, shapeType } = this;
this.groupScales = [];
const tmpMap = {};
// 遍历每一个 attrOption,各自创建 Attribute 实例
for (const attrType in attributeOption) {
if (attributeOption.hasOwnProperty(attrType)) {
const option: AttributeOption = attributeOption[attrType];
if (!option) {
return;
}
const attrCfg: AttributeInstanceCfg = {
...option,
};
const { callback, values, fields = [] } = attrCfg;
// 获取每一个字段对应的 scale
const scales = fields.map((field) => {
const scale = this.scales[field];
if (!tmpMap[field] && GROUP_ATTRS.includes(attrType)) {
const inferedScaleType = inferScaleType(scale, get(this.scaleDefs, field), attrType, this.type);
if (inferedScaleType === 'cat') {
this.groupScales.push(scale);
tmpMap[field] = true;
}
}
return scale;
});
attrCfg.scales = scales;
if (attrType !== 'position' && scales.length === 1 && scales[0].type === 'identity') {
// 用户在图形通道上声明了常量字段 color('red'), size(5)
attrCfg.values = scales[0].values;
} else if (!callback && !values) {
// 用户没有指定任何规则,则使用默认的映射规则
if (attrType === 'size') {
attrCfg.values = theme.sizes;
} else if (attrType === 'shape') {
attrCfg.values = theme.shapes[shapeType] || [];
} else if (attrType === 'color') {
if (scales.length) {
// 根据数值个数使用对应的色板
attrCfg.values = scales[0].values.length <= 10 ? theme.colors10 : theme.colors20;
} else {
attrCfg.values = theme.colors10;
}
}
}
const AttributeCtor = getAttributeClass(attrType);
attributes[attrType] = new AttributeCtor(attrCfg);
}
}
}
// 处理数据:分组 -> 数字化 -> adjust 调整
private processData(data: Data) {
this.hasSorted = false;
const { scales } = this.getAttribute('position');
const categoryScales = scales.filter((scale: Scale) => scale.isCategory);
const groupedArray = this.groupData(data); // 数据分组
const beforeAdjust = [];
for (let i = 0, len = groupedArray.length; i < len; i++) {
const subData = groupedArray[i];
const arr = [];
for (let j = 0, subLen = subData.length; j < subLen; j++) {
const originData = subData[j];
const item = {};
// tslint:disable-next-line: forin
for (const k in originData) {
item[k] = originData[k];
}
item[FIELD_ORIGIN] = originData;
// 将分类数据翻译成数据, 仅对位置相关的度量进行数字化处理
for (const scale of categoryScales) {
const field = scale.field;
item[field] = scale.translate(item[field]);
}
arr.push(item);
}
beforeAdjust.push(arr);
}
const dataArray = this.adjustData(beforeAdjust); // 进行 adjust 数据调整
this.beforeMappingData = dataArray;
return dataArray;
}
// 调整数据
private adjustData(dataArray: Data[]): Data[] {
const adjustOption = this.adjustOption;
const { intervalPadding, dodgePadding, theme } = this;
// 兼容theme配置
const maxColumnWidth = this.maxColumnWidth || theme.maxColumnWidth;
const minColumnWidth = this.minColumnWidth || theme.minColumnWidth;
const columnWidthRatio = this.columnWidthRatio || theme.columnWidthRatio;
let result = dataArray;
if (adjustOption) {
const xScale = this.getXScale();
const yScale = this.getYScale();
const xField = xScale.field;
const yField = yScale ? yScale.field : null;
const xDimensionLength = getXDimensionLength(this.coordinate);
const groupNum = xScale.values.length;
// 传入size计算相关参数,默认宽度、最大最小宽度约束
const sizeAttr = this.getAttribute('size');
let defaultSize;
if (sizeAttr) {
defaultSize = sizeAttr.values[0];
}
for (let i = 0, len = adjustOption.length; i < len; i++) {
const adjust = adjustOption[i];
const adjustCfg: AdjustInstanceCfg = {
xField,
yField,
intervalPadding,
dodgePadding,
xDimensionLength,
groupNum,
defaultSize,
maxColumnWidth,
minColumnWidth,
columnWidthRatio,
...adjust,
};
const type = adjust.type;
if (type === 'dodge') {
const adjustNames = [];
if (xScale.isCategory || xScale.type === 'identity') {
adjustNames.push('x');
} else if (!yScale) {
adjustNames.push('y');
} else {
throw new Error('dodge is not support linear attribute, please use category attribute!');
}
adjustCfg.adjustNames = adjustNames;
// 每个分组内每条柱子的宽度占比,用户不可指定,用户需要通过 columnWidthRatio 指定
// 兼容theme配置
adjustCfg.dodgeRatio = columnWidthRatio;
} else if (type === 'stack') {
const coordinate = this.coordinate;
if (!yScale) {
// 一维的情况下获取高度和默认size
adjustCfg.height = coordinate.getHeight();
const size = this.getDefaultValue('size') || 3;
adjustCfg.size = size;
}
// 不进行 transpose 时,用户又没有设置这个参数时,默认从上向下
if (!coordinate.isTransposed && isNil(adjustCfg.reverseOrder)) {
adjustCfg.reverseOrder = true;
}
}
const adjustCtor = getAdjustClass(type);
adjustCfg.dimValuesMap = {};
//生成dimValuesMap
if (xScale && xScale.values) {
adjustCfg.dimValuesMap[xScale.field] = xScale.values.map((v) => xScale.translate(v));
}
const adjustInstance = new adjustCtor(adjustCfg);
result = adjustInstance.process(result);
this.adjusts[type] = adjustInstance;
}
}
return result;
}
// 对数据进行分组
private groupData(data: Data): Data[] {
const groupScales = this.getGroupScales();
const scaleDefs = this.scaleDefs;
const appendConditions = {};
const groupFields = [];
for (let index = 0; index < groupScales.length; index++) {
const scale = groupScales[index];
const field = scale.field;
groupFields.push(field);
if (get(scaleDefs, [field, 'values'])) {
// 用户通过 view.scale() 接口指定了 values 属性
appendConditions[field] = scaleDefs[field].values;
}
}
return group(data, groupFields, appendConditions);
}
// 更新发生层叠后的数据对应的度量范围
private updateStackRange(scale: Scale, dataArray: Data[]) {
const mergeArray = flatten(dataArray);
const field = scale.field;
let min = scale.min;
let max = scale.max;
for (let index = 0; index < mergeArray.length; index++) {
const obj = mergeArray[index];
const tmpMin = Math.min.apply(null, obj[field]);
const tmpMax = Math.max.apply(null, obj[field]);
if (tmpMin < min) {
min = tmpMin;
}
if (tmpMax > max) {
max = tmpMax;
}
}
const scaleDefs = this.scaleDefs;
const cfg: LooseObject = {};
if (min < scale.min && !get(scaleDefs, [field, 'min'])) {
// 用户如果在列定义中定义了 min,则以用户定义的为准
cfg.min = min;
}
if (max > scale.max && !get(scaleDefs, [field, 'max'])) {
// 用户如果在列定义中定义了 max
cfg.max = max;
}
scale.change(cfg);
}
// 将数据映射至图形空间前的操作:排序以及关键点的生成
private beforeMapping(beforeMappingData: Data[]) {
// 当初加 clone 是因为 points 的引用关系,导致更新失败,可是现在貌似复现不出来了,所以暂时不进行 clone
// const source = clone(beforeMappingData);
const source = beforeMappingData;
if (this.sortable) {
this.sort(source);
}
if (this.generatePoints) {
// 需要生成关键点
for (let index = 0, length = source.length; index < length; index++) {
const currentData = source[index];
this.generateShapePoints(currentData);
const nextData = source[index + 1];
if (nextData) {
this.generateShapePoints(nextData);
currentData[0].nextPoints = nextData[0].points;
}
}
}
return source;
}
// 生成 shape 的关键点
private generateShapePoints(data: Data) {
const shapeFactory = this.getShapeFactory();
const shapeAttr = this.getAttribute('shape');
for (let index = 0; index < data.len