@antv/g2
Version:
the Grammar of Graphics in Javascript
700 lines (624 loc) • 21.4 kB
text/typescript
import { deepMix, each, get, isUndefined } from '@antv/util';
import { DIRECTION, COMPONENT_TYPE, LAYER } from '../../constant';
import { CircleAxis, CircleGrid, IGroup, LineAxis, LineGrid, Scale } from '../../dependents';
import { AxisCfg, AxisOption, ComponentOption } from '../../interface';
import { DEFAULT_ANIMATE_CFG } from '../../animate/';
import {
getAxisDirection,
getAxisFactorByRegion,
getAxisRegion,
getAxisThemeCfg,
getAxisTitleOptions,
getAxisTitleText,
getCircleAxisCenterRadius,
isVertical,
} from '../../util/axis';
import { getAxisOption } from '../../util/axis';
import { getCircleGridItems, getGridThemeCfg, getLineGridItems, showGrid } from '../../util/grid';
import { omit } from '../../util/helper';
import View from '../view';
import { Controller } from './base';
type Option = Record<string, AxisOption> | boolean;
type Cache = Map<string, ComponentOption>;
// update 组件的时候,忽略的数据更新
const OMIT_CFG = ['container'];
// 坐标轴默认动画配置
const AXIS_DEFAULT_ANIMATE_CFG = {
...DEFAULT_ANIMATE_CFG,
appear: null,
};
/**
* @ignore
* G2 Axis controller, will:
* - create component
* - axis
* - grid
* - life circle
*/
export default class Axis extends Controller<Option> {
/** the draw group of axis */
private axisContainer: IGroup;
private axisForeContainer: IGroup;
private gridContainer: IGroup;
private gridForeContainer: IGroup;
/** 使用 object 存储组件 */
private cache: Cache = new Map<string, ComponentOption>();
constructor(view: View) {
super(view);
// 先创建 gridContainer,将 grid 放到 axis 底层
this.gridContainer = this.view.getLayer(LAYER.BG).addGroup();
this.gridForeContainer = this.view.getLayer(LAYER.FORE).addGroup();
this.axisContainer = this.view.getLayer(LAYER.BG).addGroup();
this.axisForeContainer = this.view.getLayer(LAYER.FORE).addGroup();
}
public get name(): string {
return 'axis';
}
public init() {}
public render() {
this.update();
}
/**
* 更新组件布局,位置大小
*/
public layout() {
const coordinate = this.view.getCoordinate();
each(this.getComponents(), (co: ComponentOption) => {
const { component, direction, type, extra } = co;
const { dim, scale, alignTick } = extra;
let updated;
if (type === COMPONENT_TYPE.AXIS) {
if (coordinate.isPolar) {
if (dim === 'x') {
updated = coordinate.isTransposed
? getAxisRegion(coordinate, direction)
: getCircleAxisCenterRadius(coordinate);
} else if (dim === 'y') {
updated = coordinate.isTransposed
? getCircleAxisCenterRadius(coordinate)
: getAxisRegion(coordinate, direction);
}
} else {
updated = getAxisRegion(coordinate, direction);
}
} else if (type === COMPONENT_TYPE.GRID) {
if (coordinate.isPolar) {
let items;
if (coordinate.isTransposed) {
items =
dim === 'x'
? getCircleGridItems(coordinate, this.view.getYScales()[0], scale, alignTick, dim)
: getLineGridItems(coordinate, scale, dim, alignTick);
} else {
items =
dim === 'x'
? getLineGridItems(coordinate, scale, dim, alignTick)
: getCircleGridItems(coordinate, this.view.getXScale(), scale, alignTick, dim);
}
updated = {
items,
// coordinate 更新之后,center 也变化了
center: this.view.getCoordinate().getCenter(),
};
} else {
updated = { items: getLineGridItems(coordinate, scale, dim, alignTick) };
}
}
component.update(updated);
});
}
/**
* 更新 axis 组件
*/
public update() {
this.option = this.view.getOptions().axes;
const updatedCache = new Map<string, ComponentOption>();
this.updateXAxes(updatedCache);
this.updateYAxes(updatedCache);
// 处理完成之后,销毁删除的
// 不在处理中的
const newCache = new Map<string, ComponentOption>();
this.cache.forEach((co: ComponentOption, key: string) => {
if (updatedCache.has(key)) {
newCache.set(key, co);
} else {
// 不存在,则是所有需要被销毁的组件
co.component.destroy();
}
});
// 更新缓存
this.cache = newCache;
}
public clear() {
super.clear();
this.cache.clear();
this.gridContainer.clear();
this.gridForeContainer.clear();
this.axisContainer.clear();
this.axisForeContainer.clear();
}
public destroy() {
super.destroy();
this.gridContainer.remove(true);
this.gridForeContainer.remove(true);
this.axisContainer.remove(true);
this.axisForeContainer.remove(true);
}
/**
* @override
*/
public getComponents(): ComponentOption[] {
const co = [];
this.cache.forEach((value: ComponentOption) => {
co.push(value);
});
return co;
}
/**
* 更新 x axis
* @param updatedCache
*/
private updateXAxes(updatedCache: Cache) {
// x axis
const scale = this.view.getXScale();
if (!scale || scale.isIdentity) {
return;
}
const xAxisOption = getAxisOption(this.option, scale.field);
if (xAxisOption === false) {
return;
}
const direction = getAxisDirection(xAxisOption, DIRECTION.BOTTOM);
const layer = LAYER.BG;
const dim = 'x';
const coordinate = this.view.getCoordinate();
const axisId = this.getId('axis', scale.field);
const gridId = this.getId('grid', scale.field);
if (coordinate.isRect) {
// 1. do axis update
let axis = this.cache.get(axisId);
// 存在则更新
if (axis) {
const cfg = this.getLineAxisCfg(scale, xAxisOption, direction);
omit(cfg, OMIT_CFG);
axis.component.update(cfg);
updatedCache.set(axisId, axis);
} else {
// 不存在,则创建
axis = this.createLineAxis(scale, xAxisOption, layer, direction, dim);
this.cache.set(axisId, axis);
updatedCache.set(axisId, axis);
}
// 2. do grid update
let grid = this.cache.get(gridId);
// 存在则更新
if (grid) {
const cfg = this.getLineGridCfg(scale, xAxisOption, direction, dim);
omit(cfg, OMIT_CFG);
grid.component.update(cfg);
updatedCache.set(gridId, grid);
} else {
// 不存在则创建
grid = this.createLineGrid(scale, xAxisOption, layer, direction, dim);
if (grid) {
this.cache.set(gridId, grid);
updatedCache.set(gridId, grid);
}
}
} else if (coordinate.isPolar) {
// 1. do axis update
let axis = this.cache.get(axisId);
// 存在则更新
if (axis) {
const cfg = coordinate.isTransposed
? this.getLineAxisCfg(scale, xAxisOption, DIRECTION.RADIUS)
: this.getCircleAxisCfg(scale, xAxisOption, direction);
omit(cfg, OMIT_CFG);
axis.component.update(cfg);
updatedCache.set(axisId, axis);
} else {
// 不存在,则创建
if (coordinate.isTransposed) {
if (isUndefined(xAxisOption)) {
// 默认不渲染转置极坐标下的坐标轴
return;
} else {
// 如果用户打开了隐藏的坐标轴 chart.axis(true)/chart.axis('x', true)
// 那么对于转置了的极坐标,半径轴显示的是 x 轴对应的数据
axis = this.createLineAxis(scale, xAxisOption, layer, DIRECTION.RADIUS, dim);
}
} else {
axis = this.createCircleAxis(scale, xAxisOption, layer, direction, dim);
}
this.cache.set(axisId, axis);
updatedCache.set(axisId, axis);
}
// 2. do grid update
let grid = this.cache.get(gridId);
// 存在则更新
if (grid) {
const cfg = coordinate.isTransposed
? this.getCircleGridCfg(scale, xAxisOption, DIRECTION.RADIUS, dim)
: this.getLineGridCfg(scale, xAxisOption, DIRECTION.CIRCLE, dim);
omit(cfg, OMIT_CFG);
grid.component.update(cfg);
updatedCache.set(gridId, grid);
} else {
// 不存在则创建
if (coordinate.isTransposed) {
if (isUndefined(xAxisOption)) {
return;
} else {
grid = this.createCircleGrid(scale, xAxisOption, layer, DIRECTION.RADIUS, dim);
}
} else {
// grid,极坐标下的 x 轴网格线沿着半径方向绘制
grid = this.createLineGrid(scale, xAxisOption, layer, DIRECTION.CIRCLE, dim);
}
if (grid) {
this.cache.set(gridId, grid);
updatedCache.set(gridId, grid);
}
}
} else {
// helix and other, do not draw axis
}
}
private updateYAxes(updatedCache: Cache) {
// y axes
const yScales = this.view.getYScales();
each(yScales, (scale: Scale, idx: number) => {
// @ts-ignore
if (!scale || scale.isIdentity) {
return;
}
const { field } = scale;
const yAxisOption = getAxisOption(this.option, field);
if (yAxisOption !== false) {
const layer = LAYER.BG;
const dim = 'y';
const axisId = this.getId('axis', field);
const gridId = this.getId('grid', field);
const coordinate = this.view.getCoordinate();
if (coordinate.isRect) {
const direction = getAxisDirection(yAxisOption, idx === 0 ? DIRECTION.LEFT : DIRECTION.RIGHT);
// 1. do axis update
let axis = this.cache.get(axisId);
// 存在则更新
if (axis) {
const cfg = this.getLineAxisCfg(scale, yAxisOption, direction);
omit(cfg, OMIT_CFG);
axis.component.update(cfg);
updatedCache.set(axisId, axis);
} else {
// 不存在,则创建
axis = this.createLineAxis(scale, yAxisOption, layer, direction, dim);
this.cache.set(axisId, axis);
updatedCache.set(axisId, axis);
}
// 2. do grid update
let grid = this.cache.get(gridId);
// 存在则更新
if (grid) {
const cfg = this.getLineGridCfg(scale, yAxisOption, direction, dim);
omit(cfg, OMIT_CFG);
grid.component.update(cfg);
updatedCache.set(gridId, grid);
} else {
// 不存在则创建
grid = this.createLineGrid(scale, yAxisOption, layer, direction, dim);
if (grid) {
this.cache.set(gridId, grid);
updatedCache.set(gridId, grid);
}
}
} else if (coordinate.isPolar) {
// 1. do axis update
let axis = this.cache.get(axisId);
// 存在则更新
if (axis) {
const cfg = coordinate.isTransposed
? this.getCircleAxisCfg(scale, yAxisOption, DIRECTION.CIRCLE)
: this.getLineAxisCfg(scale, yAxisOption, DIRECTION.RADIUS);
// @ts-ignore
omit(cfg, OMIT_CFG);
axis.component.update(cfg);
updatedCache.set(axisId, axis);
} else {
// 不存在,则创建
if (coordinate.isTransposed) {
if (isUndefined(yAxisOption)) {
return;
} else {
axis = this.createCircleAxis(scale, yAxisOption, layer, DIRECTION.CIRCLE, dim);
}
} else {
axis = this.createLineAxis(scale, yAxisOption, layer, DIRECTION.RADIUS, dim);
}
this.cache.set(axisId, axis);
updatedCache.set(axisId, axis);
}
// 2. do grid update
let grid = this.cache.get(gridId);
// 存在则更新
if (grid) {
const cfg = coordinate.isTransposed
? this.getLineGridCfg(scale, yAxisOption, DIRECTION.CIRCLE, dim)
: this.getCircleGridCfg(scale, yAxisOption, DIRECTION.RADIUS, dim);
omit(cfg, OMIT_CFG);
grid.component.update(cfg);
updatedCache.set(gridId, grid);
} else {
// 不存在则创建
if (coordinate.isTransposed) {
if (isUndefined(yAxisOption)) {
return;
} else {
grid = this.createLineGrid(scale, yAxisOption, layer, DIRECTION.CIRCLE, dim);
}
} else {
grid = this.createCircleGrid(scale, yAxisOption, layer, DIRECTION.RADIUS, dim);
}
if (grid) {
this.cache.set(gridId, grid);
updatedCache.set(gridId, grid);
}
}
} else {
// helix and other, do not draw axis
}
}
});
}
/**
* 创建 line axis
* @param scale
* @param option
* @param layer
* @param direction
* @param dim
*/
private createLineAxis(
scale: Scale,
option: AxisCfg,
layer: LAYER,
direction: DIRECTION,
dim: string
): ComponentOption {
// axis
const axis = {
component: new LineAxis(this.getLineAxisCfg(scale, option, direction)),
layer,
direction: direction === DIRECTION.RADIUS ? DIRECTION.NONE : direction,
type: COMPONENT_TYPE.AXIS,
extra: { dim, scale },
};
axis.component.set('field', scale.field);
axis.component.init();
return axis;
}
private createLineGrid(
scale: Scale,
option: AxisCfg,
layer: LAYER,
direction: DIRECTION,
dim: string
): ComponentOption {
const cfg = this.getLineGridCfg(scale, option, direction, dim);
if (cfg) {
const grid = {
component: new LineGrid(cfg),
layer,
direction: DIRECTION.NONE,
type: COMPONENT_TYPE.GRID,
extra: {
dim,
scale,
alignTick: get(cfg, 'alignTick', true),
},
};
grid.component.init();
return grid;
}
}
private createCircleAxis(
scale: Scale,
option: AxisCfg,
layer: LAYER,
direction: DIRECTION,
dim: string
): ComponentOption {
const axis = {
component: new CircleAxis(this.getCircleAxisCfg(scale, option, direction)),
layer,
direction,
type: COMPONENT_TYPE.AXIS,
extra: { dim, scale },
};
axis.component.set('field', scale.field);
axis.component.init();
return axis;
}
private createCircleGrid(
scale: Scale,
option: AxisCfg,
layer: LAYER,
direction: DIRECTION,
dim: string
): ComponentOption {
const cfg = this.getCircleGridCfg(scale, option, direction, dim);
if (cfg) {
const grid = {
component: new CircleGrid(cfg),
layer,
direction: DIRECTION.NONE,
type: COMPONENT_TYPE.GRID,
extra: {
dim,
scale,
alignTick: get(cfg, 'alignTick', true),
},
};
grid.component.init();
return grid;
}
}
/**
* generate line axis cfg
* @param scale
* @param axisOption
* @param direction
* @return line axis cfg
*/
private getLineAxisCfg(scale: Scale, axisOption: AxisCfg, direction: DIRECTION) {
const container = get(axisOption, ['top']) ? this.axisForeContainer : this.axisContainer;
const coordinate = this.view.getCoordinate();
const region = getAxisRegion(coordinate, direction);
const titleText = getAxisTitleText(scale, axisOption);
const axisThemeCfg = getAxisThemeCfg(this.view.getTheme(), direction);
// the cfg order should be ensure
const optionWithTitle = get(axisOption, ['title'])
? deepMix(
{ title: { style: { text: titleText } } },
{ title: getAxisTitleOptions(this.view.getTheme(), direction, axisOption.title) },
axisOption
)
: axisOption;
const cfg = deepMix(
{
container,
...region,
ticks: scale.getTicks().map((tick) => ({ id: `${tick.tickValue}`, name: tick.text, value: tick.value })),
verticalFactor: coordinate.isPolar
? getAxisFactorByRegion(region, coordinate.getCenter()) * -1
: getAxisFactorByRegion(region, coordinate.getCenter()),
theme: axisThemeCfg,
},
axisThemeCfg,
optionWithTitle
);
const { animate, animateOption } = this.getAnimateCfg(cfg);
cfg.animateOption = animateOption;
cfg.animate = animate;
// 计算 verticalLimitLength
const isAxisVertical = isVertical(region);
// TODO: 1 / 3 等默认值需要有一个全局的配置的地方
const verticalLimitLength = get(cfg, 'verticalLimitLength', isAxisVertical ? 1 / 3 : 1 / 2);
if (verticalLimitLength <= 1) {
// 配置的相对值,相对于画布
const canvasWidth = this.view.getCanvas().get('width');
const canvasHeight = this.view.getCanvas().get('height');
cfg.verticalLimitLength = verticalLimitLength * (isAxisVertical ? canvasWidth : canvasHeight);
}
return cfg;
}
/**
* generate line grid cfg
* @param scale
* @param axisOption
* @param direction
* @param dim
* @return line grid cfg
*/
private getLineGridCfg(scale: Scale, axisOption: AxisCfg, direction: DIRECTION, dim: string) {
if (!showGrid(getAxisThemeCfg(this.view.getTheme(), direction), axisOption)) {
return undefined;
}
const gridThemeCfg = getGridThemeCfg(this.view.getTheme(), direction);
// the cfg order should be ensure
// grid 动画以 axis 为准
const gridCfg = deepMix(
{
container: get(axisOption, ['top']) ? this.gridForeContainer : this.gridContainer,
},
gridThemeCfg,
get(axisOption, 'grid'),
this.getAnimateCfg(axisOption)
);
gridCfg.items = getLineGridItems(this.view.getCoordinate(), scale, dim, get(gridCfg, 'alignTick', true));
return gridCfg;
}
/**
* generate circle axis cfg
* @param scale
* @param axisOption
* @param direction
* @return circle axis cfg
*/
private getCircleAxisCfg(scale: Scale, axisOption: AxisCfg, direction: DIRECTION) {
const container = get(axisOption, ['top']) ? this.axisForeContainer : this.axisContainer;
const coordinate = this.view.getCoordinate();
const ticks = scale.getTicks().map((tick) => ({ id: `${tick.tickValue}`, name: tick.text, value: tick.value }));
if (!scale.isCategory && Math.abs(coordinate.endAngle - coordinate.startAngle) === Math.PI * 2) {
// x 轴对应的值如果是非 cat 类型,在整圆的情况下坐标轴第一个和最后一个文本会重叠,默认只展示第一个文本
ticks.pop();
}
const titleText = getAxisTitleText(scale, axisOption);
const axisThemeCfg = getAxisThemeCfg(this.view.getTheme(), DIRECTION.CIRCLE);
// the cfg order should be ensure
const optionWithTitle = get(axisOption, ['title'])
? deepMix(
{ title: { style: { text: titleText } } },
{ title: getAxisTitleOptions(this.view.getTheme(), direction, axisOption.title) },
axisOption
)
: axisOption;
const cfg = deepMix(
{
container,
...getCircleAxisCenterRadius(this.view.getCoordinate()),
ticks,
verticalFactor: 1,
theme: axisThemeCfg,
},
axisThemeCfg,
optionWithTitle
);
const { animate, animateOption } = this.getAnimateCfg(cfg);
cfg.animate = animate;
cfg.animateOption = animateOption;
return cfg;
}
/**
* generate circle grid cfg
* @param scale
* @param axisOption
* @param direction
* @return circle grid cfg
*/
private getCircleGridCfg(scale: Scale, axisOption: AxisCfg, direction: DIRECTION, dim: string) {
if (!showGrid(getAxisThemeCfg(this.view.getTheme(), direction), axisOption)) {
return undefined;
}
// the cfg order should be ensure
// grid 动画以 axis 为准
const gridThemeCfg = getGridThemeCfg(this.view.getTheme(), DIRECTION.RADIUS);
const gridCfg = deepMix(
{
container: get(axisOption, ['top']) ? this.gridForeContainer : this.gridContainer,
center: this.view.getCoordinate().getCenter(),
},
gridThemeCfg,
get(axisOption, 'grid'),
this.getAnimateCfg(axisOption)
);
const alignTick = get(gridCfg, 'alignTick', true);
const verticalScale = dim === 'x' ? this.view.getYScales()[0] : this.view.getXScale();
gridCfg.items = getCircleGridItems(this.view.getCoordinate(), verticalScale, scale, alignTick, dim);
// the cfg order should be ensure
// grid 动画以 axis 为准
return gridCfg;
}
private getId(name: string, key: string): string {
const coordinate = this.view.getCoordinate();
// 坐标系类型也作为组件的 key
return `${name}-${key}-${coordinate.type}`;
}
private getAnimateCfg(cfg) {
return {
animate: this.view.getOptions().animate && get(cfg, 'animate'), // 如果 view 关闭动画,则不执行动画
animateOption:
cfg && cfg.animateOption ? deepMix({}, AXIS_DEFAULT_ANIMATE_CFG, cfg.animateOption) : AXIS_DEFAULT_ANIMATE_CFG,
};
}
}