555 lines
20 KiB
Java
555 lines
20 KiB
Java
import { __assign, __extends } from "tslib";
|
||
import { contains, deepMix, each, get, isArray, isFunction, isNil, isString, keys, upperFirst } from '@antv/util';
|
||
import { Annotation as AnnotationComponent } from '../../dependents';
|
||
import { DEFAULT_ANIMATE_CFG } from '../../animate/';
|
||
import { COMPONENT_TYPE, DIRECTION, LAYER, VIEW_LIFE_CIRCLE } from '../../constant';
|
||
import { getAngleByPoint, getDistanceToCenter } from '../../util/coordinate';
|
||
import { omit } from '../../util/helper';
|
||
import { Controller } from './base';
|
||
/**
|
||
* Annotation controller, 主要作用:
|
||
* 1. 创建 Annotation: line、text、arc ...
|
||
* 2. 生命周期: init、layout、render、clear、destroy
|
||
*/
|
||
var Annotation = /** @class */ (function (_super) {
|
||
__extends(Annotation, _super);
|
||
function Annotation(view) {
|
||
var _this = _super.call(this, view) || this;
|
||
/* 组件更新的 cache,组件配置 object : 组件 */
|
||
_this.cache = new Map();
|
||
_this.foregroundContainer = _this.view.getLayer(LAYER.FORE).addGroup();
|
||
_this.backgroundContainer = _this.view.getLayer(LAYER.BG).addGroup();
|
||
_this.option = [];
|
||
return _this;
|
||
}
|
||
Object.defineProperty(Annotation.prototype, "name", {
|
||
get: function () {
|
||
return 'annotation';
|
||
},
|
||
enumerable: false,
|
||
configurable: true
|
||
});
|
||
Annotation.prototype.init = function () { };
|
||
Annotation.prototype.layout = function () {
|
||
var _this = this;
|
||
var components = this.getComponents();
|
||
var updateComponentFn = function (co) {
|
||
var component = co.component, extra = co.extra;
|
||
var type = extra.type;
|
||
var theme = _this.getAnnotationTheme(type);
|
||
component.update(_this.getAnnotationCfg(type, extra, theme));
|
||
};
|
||
var createComponentFn = function (option) {
|
||
var co = _this.createAnnotation(option);
|
||
if (co) {
|
||
co.component.init();
|
||
// Note:regionFilter 特殊处理,regionFilter需要取到 Geometry 中的 Shape,需要在 view render 之后处理
|
||
// 其他组件使用外层的统一 render 逻辑
|
||
if (option.type === 'regionFilter') {
|
||
co.component.render();
|
||
}
|
||
// 缓存起来
|
||
_this.cache.set(option, co);
|
||
}
|
||
};
|
||
if (components.length) {
|
||
each(components, function (co) {
|
||
var component = co.component;
|
||
if (component.get('type') === 'regionFilter') {
|
||
// regionFilter 依赖绘制后的 Geometry Shapes
|
||
_this.view.once(VIEW_LIFE_CIRCLE.AFTER_RENDER, function () {
|
||
updateComponentFn(co);
|
||
});
|
||
}
|
||
else {
|
||
updateComponentFn(co);
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
each(this.option, function (option) {
|
||
if (option.type === 'regionFilter') {
|
||
_this.view.once(VIEW_LIFE_CIRCLE.AFTER_RENDER, function () {
|
||
// regionFilter 依赖绘制后的 Geometry Shapes
|
||
createComponentFn(option);
|
||
});
|
||
}
|
||
else {
|
||
createComponentFn(option);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
Annotation.prototype.render = function () {
|
||
// 因为 Annotation 不参与布局,但是渲染的位置依赖于坐标系,所以可以将绘制阶段延迟到 layout() 进行
|
||
};
|
||
/**
|
||
* 更新
|
||
*/
|
||
Annotation.prototype.update = function () {
|
||
var _this = this;
|
||
// 已经处理过的 legend
|
||
var updated = new WeakMap();
|
||
var updateComponentFn = function (option) {
|
||
var type = option.type;
|
||
var theme = _this.getAnnotationTheme(type);
|
||
var cfg = _this.getAnnotationCfg(type, option, theme);
|
||
var existCo = _this.cache.get(option);
|
||
// 存在,则更新
|
||
if (existCo) {
|
||
// 忽略掉一些配置
|
||
omit(cfg, ['container']);
|
||
existCo.component.update(cfg);
|
||
updated.set(option, true);
|
||
}
|
||
else {
|
||
// 不存在,则创建
|
||
var co = _this.createAnnotation(option);
|
||
if (co) {
|
||
co.component.init();
|
||
// Note:regionFilter 特殊处理,regionFilter需要取到 Geometry 中的 Shape,需要在 view render 之后处理
|
||
// 其他组件使用外层的统一 render 逻辑
|
||
if (option.type === 'regionFilter') {
|
||
co.component.render();
|
||
}
|
||
// 缓存起来
|
||
_this.cache.set(option, co);
|
||
updated.set(option, true);
|
||
}
|
||
}
|
||
};
|
||
this.view.once(VIEW_LIFE_CIRCLE.AFTER_RENDER, function () {
|
||
// 先看是否有 regionFilter 要更新
|
||
each(_this.option, function (option) {
|
||
if (option.type === 'regionFilter') {
|
||
updateComponentFn(option);
|
||
}
|
||
});
|
||
// 处理完成之后,销毁删除的
|
||
// 不在处理中的
|
||
var newCache = new Map();
|
||
_this.cache.forEach(function (value, key) {
|
||
if (updated.has(key)) {
|
||
newCache.set(key, value);
|
||
}
|
||
else {
|
||
// 不存在,则是所有需要被销毁的组件
|
||
value.component.destroy();
|
||
}
|
||
});
|
||
// 更新缓存
|
||
_this.cache = newCache;
|
||
});
|
||
each(this.option, function (option) {
|
||
if (option.type !== 'regionFilter') {
|
||
updateComponentFn(option);
|
||
}
|
||
});
|
||
};
|
||
/**
|
||
* 清空
|
||
* @param includeOption 是否清空 option 配置项
|
||
*/
|
||
Annotation.prototype.clear = function (includeOption) {
|
||
if (includeOption === void 0) { includeOption = false; }
|
||
_super.prototype.clear.call(this);
|
||
this.cache.clear();
|
||
this.foregroundContainer.clear();
|
||
this.backgroundContainer.clear();
|
||
// clear all option
|
||
if (includeOption) {
|
||
this.option = [];
|
||
}
|
||
};
|
||
Annotation.prototype.destroy = function () {
|
||
this.clear(true);
|
||
this.foregroundContainer.remove(true);
|
||
this.backgroundContainer.remove(true);
|
||
};
|
||
/**
|
||
* 复写基类的方法
|
||
*/
|
||
Annotation.prototype.getComponents = function () {
|
||
var co = [];
|
||
this.cache.forEach(function (value) {
|
||
co.push(value);
|
||
});
|
||
return co;
|
||
};
|
||
Annotation.prototype.createAnnotation = function (option) {
|
||
var type = option.type;
|
||
var Ctor = AnnotationComponent[upperFirst(type)];
|
||
if (Ctor) {
|
||
var theme = this.getAnnotationTheme(type);
|
||
var cfg = this.getAnnotationCfg(type, option, theme);
|
||
var annotation = new Ctor(cfg);
|
||
return {
|
||
component: annotation,
|
||
layer: this.isTop(cfg) ? LAYER.FORE : LAYER.BG,
|
||
direction: DIRECTION.NONE,
|
||
type: COMPONENT_TYPE.ANNOTATION,
|
||
extra: option,
|
||
};
|
||
}
|
||
};
|
||
// APIs for creating annotation component
|
||
Annotation.prototype.annotation = function (option) {
|
||
this.option.push(option);
|
||
};
|
||
/**
|
||
* 创建 Arc
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.arc = function (option) {
|
||
this.annotation(__assign({ type: 'arc' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 image
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.image = function (option) {
|
||
this.annotation(__assign({ type: 'image' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 Line
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.line = function (option) {
|
||
this.annotation(__assign({ type: 'line' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 Region
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.region = function (option) {
|
||
this.annotation(__assign({ type: 'region' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 Text
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.text = function (option) {
|
||
this.annotation(__assign({ type: 'text' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 DataMarker
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.dataMarker = function (option) {
|
||
this.annotation(__assign({ type: 'dataMarker' }, option));
|
||
return this;
|
||
};
|
||
/**
|
||
* 创建 DataRegion
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.dataRegion = function (option) {
|
||
this.annotation(__assign({ type: 'dataRegion' }, option));
|
||
};
|
||
/**
|
||
* 创建 RegionFilter
|
||
* @param option
|
||
* @returns AnnotationController
|
||
*/
|
||
Annotation.prototype.regionFilter = function (option) {
|
||
this.annotation(__assign({ type: 'regionFilter' }, option));
|
||
};
|
||
// end API
|
||
/**
|
||
* parse the point position to [x, y]
|
||
* @param p Position
|
||
* @returns { x, y }
|
||
*/
|
||
Annotation.prototype.parsePosition = function (p) {
|
||
var xScale = this.view.getXScale();
|
||
// 转成 object
|
||
var yScales = this.view.getScalesByDim('y');
|
||
var position = isFunction(p) ? p.call(null, xScale, yScales) : p;
|
||
var x = 0;
|
||
var y = 0;
|
||
// 入参是 [24, 24] 这类时
|
||
if (isArray(position)) {
|
||
var xPos = position[0], yPos = position[1];
|
||
// 如果数据格式是 ['50%', '50%'] 的格式
|
||
// fix: 原始数据中可能会包含 'xxx5%xxx' 这样的数据,需要判断下 https://github.com/antvis/f2/issues/590
|
||
// @ts-ignore
|
||
if (isString(xPos) && xPos.indexOf('%') !== -1 && !isNaN(xPos.slice(0, -1))) {
|
||
return this.parsePercentPosition(position);
|
||
}
|
||
x = this.getNormalizedValue(xPos, xScale);
|
||
y = this.getNormalizedValue(yPos, Object.values(yScales)[0]);
|
||
}
|
||
else if (!isNil(position)) {
|
||
// 入参是 object 结构,数据点
|
||
for (var _i = 0, _a = keys(position); _i < _a.length; _i++) {
|
||
var key = _a[_i];
|
||
var value = position[key];
|
||
if (key === xScale.field) {
|
||
x = this.getNormalizedValue(value, xScale);
|
||
}
|
||
if (yScales[key]) {
|
||
y = this.getNormalizedValue(value, yScales[key]);
|
||
}
|
||
}
|
||
}
|
||
return this.view.getCoordinate().convert({ x: x, y: y });
|
||
};
|
||
/**
|
||
* parse all the points between start and end
|
||
* @param start
|
||
* @param end
|
||
* @return Point[]
|
||
*/
|
||
Annotation.prototype.getRegionPoints = function (start, end) {
|
||
var _this = this;
|
||
var xScale = this.view.getXScale();
|
||
var yScales = this.view.getScalesByDim('y');
|
||
var yScale = Object.values(yScales)[0];
|
||
var xField = xScale.field;
|
||
var viewData = this.view.getData();
|
||
var startXValue = isArray(start) ? start[0] : start[xField];
|
||
var endXValue = isArray(end) ? end[0] : end[xField];
|
||
var arr = [];
|
||
var startIndex;
|
||
each(viewData, function (item, idx) {
|
||
if (item[xField] === startXValue) {
|
||
startIndex = idx;
|
||
}
|
||
if (idx >= startIndex) {
|
||
var point = _this.parsePosition([item[xField], item[yScale.field]]);
|
||
if (point) {
|
||
arr.push(point);
|
||
}
|
||
}
|
||
if (item[xField] === endXValue) {
|
||
return false;
|
||
}
|
||
});
|
||
return arr;
|
||
};
|
||
/**
|
||
* parse the value position
|
||
* @param val
|
||
* @param scale
|
||
*/
|
||
Annotation.prototype.getNormalizedValue = function (val, scale) {
|
||
var result;
|
||
var scaled;
|
||
switch (val) {
|
||
case 'start':
|
||
result = 0;
|
||
break;
|
||
case 'end':
|
||
result = 1;
|
||
break;
|
||
case 'median': {
|
||
scaled = scale.isCategory ? (scale.values.length - 1) / 2 : (scale.min + scale.max) / 2;
|
||
result = scale.scale(scaled);
|
||
break;
|
||
}
|
||
case 'min':
|
||
case 'max':
|
||
if (scale.isCategory) {
|
||
scaled = val === 'min' ? 0 : scale.values.length - 1;
|
||
}
|
||
else {
|
||
scaled = scale[val];
|
||
}
|
||
result = scale.scale(scaled);
|
||
break;
|
||
default:
|
||
result = scale.scale(val);
|
||
}
|
||
return result;
|
||
};
|
||
/**
|
||
* parse percent position
|
||
* @param position
|
||
*/
|
||
Annotation.prototype.parsePercentPosition = function (position) {
|
||
var xPercent = parseFloat(position[0]) / 100;
|
||
var yPercent = parseFloat(position[1]) / 100;
|
||
var coordinate = this.view.getCoordinate();
|
||
var start = coordinate.start, end = coordinate.end;
|
||
var topLeft = {
|
||
x: Math.min(start.x, end.x),
|
||
y: Math.min(start.y, end.y),
|
||
};
|
||
var x = coordinate.getWidth() * xPercent + topLeft.x;
|
||
var y = coordinate.getHeight() * yPercent + topLeft.y;
|
||
return { x: x, y: y };
|
||
};
|
||
/**
|
||
* get coordinate bbox
|
||
*/
|
||
Annotation.prototype.getCoordinateBBox = function () {
|
||
var coordinate = this.view.getCoordinate();
|
||
var start = coordinate.start, end = coordinate.end;
|
||
var width = coordinate.getWidth();
|
||
var height = coordinate.getHeight();
|
||
var topLeft = {
|
||
x: Math.min(start.x, end.x),
|
||
y: Math.min(start.y, end.y),
|
||
};
|
||
return {
|
||
x: topLeft.x,
|
||
y: topLeft.y,
|
||
minX: topLeft.x,
|
||
minY: topLeft.y,
|
||
maxX: topLeft.x + width,
|
||
maxY: topLeft.y + height,
|
||
width: width,
|
||
height: height,
|
||
};
|
||
};
|
||
/**
|
||
* get annotation component config by different type
|
||
* @param type
|
||
* @param option 用户的配置
|
||
* @param theme
|
||
*/
|
||
Annotation.prototype.getAnnotationCfg = function (type, option, theme) {
|
||
var coordinate = this.view.getCoordinate();
|
||
var o = {};
|
||
if (isNil(option)) {
|
||
return null;
|
||
}
|
||
if (type === 'arc') {
|
||
var _a = option, start = _a.start, end = _a.end;
|
||
var sp = this.parsePosition(start);
|
||
var ep = this.parsePosition(end);
|
||
var startAngle = getAngleByPoint(coordinate, sp);
|
||
var endAngle = getAngleByPoint(coordinate, ep);
|
||
if (startAngle > endAngle) {
|
||
endAngle = Math.PI * 2 + endAngle;
|
||
}
|
||
o = {
|
||
center: coordinate.getCenter(),
|
||
radius: getDistanceToCenter(coordinate, sp),
|
||
startAngle: startAngle,
|
||
endAngle: endAngle,
|
||
};
|
||
}
|
||
else if (type === 'image') {
|
||
var _b = option, start = _b.start, end = _b.end;
|
||
o = {
|
||
start: this.parsePosition(start),
|
||
end: this.parsePosition(end),
|
||
src: option.src,
|
||
};
|
||
}
|
||
else if (type === 'line') {
|
||
var _c = option, start = _c.start, end = _c.end;
|
||
o = {
|
||
start: this.parsePosition(start),
|
||
end: this.parsePosition(end),
|
||
text: get(option, 'text', null),
|
||
};
|
||
}
|
||
else if (type === 'region') {
|
||
var _d = option, start = _d.start, end = _d.end;
|
||
o = {
|
||
start: this.parsePosition(start),
|
||
end: this.parsePosition(end),
|
||
};
|
||
}
|
||
else if (type === 'text') {
|
||
var _e = option, position = _e.position, rotate = _e.rotate;
|
||
o = __assign(__assign({}, this.parsePosition(position)), { content: option.content, rotate: rotate });
|
||
}
|
||
else if (type === 'dataMarker') {
|
||
var _f = option, position = _f.position, point = _f.point, line = _f.line, text = _f.text, autoAdjust = _f.autoAdjust, direction = _f.direction;
|
||
o = __assign(__assign({}, this.parsePosition(position)), { coordinateBBox: this.getCoordinateBBox(), point: point,
|
||
line: line,
|
||
text: text,
|
||
autoAdjust: autoAdjust,
|
||
direction: direction });
|
||
}
|
||
else if (type === 'dataRegion') {
|
||
var _g = option, start = _g.start, end = _g.end, region = _g.region, text = _g.text, lineLength = _g.lineLength;
|
||
o = {
|
||
points: this.getRegionPoints(start, end),
|
||
region: region,
|
||
text: text,
|
||
lineLength: lineLength,
|
||
};
|
||
}
|
||
else if (type === 'regionFilter') {
|
||
var _h = option, start = _h.start, end = _h.end, apply_1 = _h.apply, color = _h.color;
|
||
var geometries = this.view.geometries;
|
||
var shapes_1 = [];
|
||
var addShapes_1 = function (item) {
|
||
if (!item) {
|
||
return;
|
||
}
|
||
if (item.isGroup()) {
|
||
item.getChildren().forEach(function (child) { return addShapes_1(child); });
|
||
}
|
||
else {
|
||
shapes_1.push(item);
|
||
}
|
||
};
|
||
each(geometries, function (geom) {
|
||
if (apply_1) {
|
||
if (contains(apply_1, geom.type)) {
|
||
each(geom.elements, function (elem) {
|
||
addShapes_1(elem.shape);
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
each(geom.elements, function (elem) {
|
||
addShapes_1(elem.shape);
|
||
});
|
||
}
|
||
});
|
||
o = {
|
||
color: color,
|
||
shapes: shapes_1,
|
||
start: this.parsePosition(start),
|
||
end: this.parsePosition(end),
|
||
};
|
||
}
|
||
// 合并主题,用户配置优先级高于默认主题
|
||
var cfg = deepMix({}, theme, __assign(__assign({}, o), { top: option.top, style: option.style, offsetX: option.offsetX, offsetY: option.offsetY }));
|
||
cfg.container = this.getComponentContainer(cfg);
|
||
cfg.animate = this.view.getOptions().animate && cfg.animate && get(option, 'animate', cfg.animate); // 如果 view 关闭动画,则不执行
|
||
cfg.animateOption = deepMix({}, DEFAULT_ANIMATE_CFG, cfg.animateOption, option.animateOption);
|
||
return cfg;
|
||
};
|
||
/**
|
||
* is annotation render on top
|
||
* @param option
|
||
* @return whethe on top
|
||
*/
|
||
Annotation.prototype.isTop = function (option) {
|
||
return get(option, 'top', true);
|
||
};
|
||
/**
|
||
* get the container by option.top
|
||
* default is on top
|
||
* @param option
|
||
* @returns the container
|
||
*/
|
||
Annotation.prototype.getComponentContainer = function (option) {
|
||
return this.isTop(option) ? this.foregroundContainer : this.backgroundContainer;
|
||
};
|
||
Annotation.prototype.getAnnotationTheme = function (type) {
|
||
return get(this.view.getTheme(), ['components', 'annotation', type], {});
|
||
};
|
||
return Annotation;
|
||
}(Controller));
|
||
export default Annotation;
|
||
//# sourceMappingURL=annotation.js.map |