497 lines
18 KiB
Java
497 lines
18 KiB
Java
import { __extends } from "tslib";
|
||
import { deepMix, each, find, get, head, isBoolean, last } from '@antv/util';
|
||
import { COMPONENT_MAX_VIEW_PERCENTAGE, COMPONENT_TYPE, DIRECTION, LAYER } from '../../constant';
|
||
import { CategoryLegend, ContinuousLegend } from '../../dependents';
|
||
import { DEFAULT_ANIMATE_CFG } from '../../animate';
|
||
import { BBox } from '../../util/bbox';
|
||
import { directionToPosition } from '../../util/direction';
|
||
import { omit } from '../../util/helper';
|
||
import { getCustomLegendItems, getLegendItems, getLegendLayout } from '../../util/legend';
|
||
import { getName } from '../../util/scale';
|
||
import { Controller } from './base';
|
||
/**
|
||
* 从配置中获取单个字段的 legend 配置
|
||
* @param legends
|
||
* @param field
|
||
* @returns the option of one legend field
|
||
*/
|
||
function getLegendOption(legends, field) {
|
||
if (isBoolean(legends)) {
|
||
return legends === false ? false : {};
|
||
}
|
||
return get(legends, [field], legends);
|
||
}
|
||
function getDirection(legendOption) {
|
||
return get(legendOption, 'position', DIRECTION.BOTTOM);
|
||
}
|
||
/**
|
||
* @ignore
|
||
* legend Controller
|
||
*/
|
||
var Legend = /** @class */ (function (_super) {
|
||
__extends(Legend, _super);
|
||
function Legend(view) {
|
||
var _this = _super.call(this, view) || this;
|
||
_this.container = _this.view.getLayer(LAYER.FORE).addGroup();
|
||
return _this;
|
||
}
|
||
Object.defineProperty(Legend.prototype, "name", {
|
||
get: function () {
|
||
return 'legend';
|
||
},
|
||
enumerable: false,
|
||
configurable: true
|
||
});
|
||
Legend.prototype.init = function () { };
|
||
/**
|
||
* render the legend component by legend options
|
||
*/
|
||
Legend.prototype.render = function () {
|
||
var _this = this;
|
||
this.option = this.view.getOptions().legends;
|
||
var doEachLegend = function (geometry, attr, scale) {
|
||
var legend = _this.createFieldLegend(geometry, attr, scale);
|
||
if (legend) {
|
||
legend.component.init();
|
||
_this.components.push(legend);
|
||
}
|
||
};
|
||
// 全局自定义图例
|
||
if (get(this.option, 'custom')) {
|
||
var component = this.createCustomLegend(undefined, undefined, undefined, this.option);
|
||
if (component) {
|
||
component.init();
|
||
var layer = LAYER.FORE;
|
||
var direction = getDirection(this.option);
|
||
this.components.push({
|
||
id: 'global-custom',
|
||
component: component,
|
||
layer: layer,
|
||
direction: direction,
|
||
type: COMPONENT_TYPE.LEGEND,
|
||
extra: undefined,
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
// 遍历处理每一个创建逻辑
|
||
this.loopLegends(doEachLegend);
|
||
}
|
||
};
|
||
/**
|
||
* layout legend
|
||
* 计算出 legend 的 direction 位置 x, y
|
||
*/
|
||
Legend.prototype.layout = function () {
|
||
var _this = this;
|
||
this.layoutBBox = this.view.viewBBox;
|
||
each(this.components, function (co) {
|
||
var component = co.component, direction = co.direction;
|
||
var layout = getLegendLayout(direction);
|
||
var maxSize = _this.getCategoryLegendSizeCfg(layout);
|
||
var maxWidth = component.get('maxWidth');
|
||
var maxHeight = component.get('maxHeight');
|
||
// 先更新 maxSize,更新 layoutBBox,以便计算正确的 x y
|
||
component.update({
|
||
maxWidth: Math.min(maxSize.maxWidth, maxWidth || 0),
|
||
maxHeight: Math.min(maxSize.maxHeight, maxHeight || 0),
|
||
});
|
||
var bboxObject = component.getLayoutBBox(); // 这里只需要他的 width、height 信息做位置调整
|
||
var bbox = new BBox(bboxObject.x, bboxObject.y, bboxObject.width, bboxObject.height);
|
||
var _a = directionToPosition(_this.view.coordinateBBox, bbox, direction), x1 = _a[0], y1 = _a[1];
|
||
var _b = directionToPosition(_this.layoutBBox, bbox, direction), x2 = _b[0], y2 = _b[1];
|
||
var x = 0;
|
||
var y = 0;
|
||
// 因为 legend x y 要和 coordinateBBox 对齐,所以要做一个简单的判断
|
||
if (direction.startsWith('top') || direction.startsWith('bottom')) {
|
||
x = x1;
|
||
y = y2;
|
||
}
|
||
else {
|
||
x = x2;
|
||
y = y1;
|
||
}
|
||
// 更新位置
|
||
component.update({
|
||
x: x,
|
||
y: y,
|
||
});
|
||
_this.layoutBBox = _this.layoutBBox.cut(bbox, direction);
|
||
});
|
||
};
|
||
/**
|
||
* legend 的更新逻辑
|
||
*/
|
||
Legend.prototype.update = function () {
|
||
var _this = this;
|
||
this.option = this.view.getOptions().legends;
|
||
// 已经处理过的 legend
|
||
var updated = {};
|
||
var eachLegend = function (geometry, attr, scale) {
|
||
var id = _this.getId(scale.field);
|
||
var existCo = _this.getComponentById(id);
|
||
// 存在则 update
|
||
if (existCo) {
|
||
var cfg = void 0;
|
||
var legendOption = getLegendOption(_this.option, scale.field);
|
||
// if the legend option is not false, means legend should be created.
|
||
if (legendOption !== false) {
|
||
if (get(legendOption, 'custom')) {
|
||
cfg = _this.getCategoryCfg(geometry, attr, scale, legendOption, true);
|
||
}
|
||
else {
|
||
if (scale.isLinear) {
|
||
// linear field, create continuous legend
|
||
cfg = _this.getContinuousCfg(geometry, attr, scale, legendOption);
|
||
}
|
||
else if (scale.isCategory) {
|
||
// category field, create category legend
|
||
cfg = _this.getCategoryCfg(geometry, attr, scale, legendOption);
|
||
}
|
||
}
|
||
}
|
||
// 如果 cfg 为空,则不在 updated 标记,那么会在后面逻辑中删除
|
||
if (cfg) {
|
||
// omit 掉一些属性,比如 container 等
|
||
omit(cfg, ['container']);
|
||
existCo.direction = getDirection(legendOption);
|
||
existCo.component.update(cfg);
|
||
// 标记为新的
|
||
updated[id] = true;
|
||
}
|
||
}
|
||
else {
|
||
// 不存在则 create
|
||
var legend = _this.createFieldLegend(geometry, attr, scale);
|
||
if (legend) {
|
||
legend.component.init();
|
||
_this.components.push(legend);
|
||
// 标记为新的
|
||
updated[id] = true;
|
||
}
|
||
}
|
||
};
|
||
// 全局自定义图例
|
||
if (get(this.option, 'custom')) {
|
||
var id = 'global-custom';
|
||
var existCo = this.getComponentById(id);
|
||
if (existCo) {
|
||
var customCfg = this.getCategoryCfg(undefined, undefined, undefined, this.option, true);
|
||
omit(customCfg, ['container']);
|
||
existCo.component.update(customCfg);
|
||
updated[id] = true;
|
||
}
|
||
else {
|
||
var component = this.createCustomLegend(undefined, undefined, undefined, this.option);
|
||
if (component) {
|
||
component.init();
|
||
var layer = LAYER.FORE;
|
||
var direction = getDirection(this.option);
|
||
this.components.push({
|
||
id: id,
|
||
component: component,
|
||
layer: layer,
|
||
direction: direction,
|
||
type: COMPONENT_TYPE.LEGEND,
|
||
extra: undefined,
|
||
});
|
||
// 标记为更新
|
||
updated[id] = true;
|
||
}
|
||
}
|
||
}
|
||
else {
|
||
// 遍历处理每一个创建逻辑
|
||
this.loopLegends(eachLegend);
|
||
}
|
||
// 处理完成之后,销毁删除的
|
||
// 不在处理中的
|
||
var components = [];
|
||
each(this.getComponents(), function (co) {
|
||
if (updated[co.id]) {
|
||
components.push(co);
|
||
}
|
||
else {
|
||
co.component.destroy();
|
||
}
|
||
});
|
||
// 更新当前已有的 components
|
||
this.components = components;
|
||
};
|
||
Legend.prototype.clear = function () {
|
||
_super.prototype.clear.call(this);
|
||
this.container.clear();
|
||
};
|
||
Legend.prototype.destroy = function () {
|
||
_super.prototype.destroy.call(this);
|
||
this.container.remove(true);
|
||
};
|
||
/**
|
||
* 递归获取所有的 Geometry
|
||
*/
|
||
Legend.prototype.getGeometries = function (view) {
|
||
var _this = this;
|
||
var geometries = view.geometries;
|
||
each(view.views, function (v) {
|
||
geometries = geometries.concat(_this.getGeometries(v));
|
||
});
|
||
return geometries;
|
||
};
|
||
/**
|
||
* 遍历 Geometry,处理 legend 逻辑
|
||
* @param doEach 每个 loop 中的处理方法
|
||
*/
|
||
Legend.prototype.loopLegends = function (doEach) {
|
||
var isRootView = this.view.getRootView() === this.view;
|
||
// 非根 view,不处理 legend
|
||
if (!isRootView) {
|
||
return;
|
||
}
|
||
// 递归 view 中所有的 Geometry,进行创建 legend
|
||
var geometries = this.getGeometries(this.view);
|
||
var looped = {}; // 防止一个字段创建两个 legend
|
||
each(geometries, function (geometry) {
|
||
var attributes = geometry.getGroupAttributes();
|
||
each(attributes, function (attr) {
|
||
var scale = attr.getScale(attr.type);
|
||
// 如果在视觉通道上映射常量值,如 size(2) shape('circle') 不创建 legend
|
||
if (!scale || scale.type === 'identity' || looped[scale.field]) {
|
||
return;
|
||
}
|
||
doEach(geometry, attr, scale);
|
||
looped[scale.field] = true;
|
||
});
|
||
});
|
||
};
|
||
/**
|
||
* 创建一个 legend
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
*/
|
||
Legend.prototype.createFieldLegend = function (geometry, attr, scale) {
|
||
var component;
|
||
var legendOption = getLegendOption(this.option, scale.field);
|
||
var layer = LAYER.FORE;
|
||
var direction = getDirection(legendOption);
|
||
// if the legend option is not false, means legend should be created.
|
||
if (legendOption !== false) {
|
||
if (get(legendOption, 'custom')) {
|
||
component = this.createCustomLegend(geometry, attr, scale, legendOption);
|
||
}
|
||
else {
|
||
if (scale.isLinear) {
|
||
// linear field, create continuous legend
|
||
component = this.createContinuousLegend(geometry, attr, scale, legendOption);
|
||
}
|
||
else if (scale.isCategory) {
|
||
// category field, create category legend
|
||
component = this.createCategoryLegend(geometry, attr, scale, legendOption);
|
||
}
|
||
}
|
||
}
|
||
if (component) {
|
||
component.set('field', scale.field);
|
||
return {
|
||
id: this.getId(scale.field),
|
||
component: component,
|
||
layer: layer,
|
||
direction: direction,
|
||
type: COMPONENT_TYPE.LEGEND,
|
||
extra: { scale: scale },
|
||
};
|
||
}
|
||
};
|
||
/**
|
||
* 自定义图例使用 category 图例去渲染
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
* @param legendOption
|
||
*/
|
||
Legend.prototype.createCustomLegend = function (geometry, attr, scale, legendOption) {
|
||
// 直接使用 分类图例渲染
|
||
var cfg = this.getCategoryCfg(geometry, attr, scale, legendOption, true);
|
||
return new CategoryLegend(cfg);
|
||
};
|
||
/**
|
||
* 创建连续图例
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
* @param legendOption
|
||
*/
|
||
Legend.prototype.createContinuousLegend = function (geometry, attr, scale, legendOption) {
|
||
var cfg = this.getContinuousCfg(geometry, attr, scale, legendOption);
|
||
return new ContinuousLegend(cfg);
|
||
};
|
||
/**
|
||
* 创建分类图例
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
* @param legendOption
|
||
*/
|
||
Legend.prototype.createCategoryLegend = function (geometry, attr, scale, legendOption) {
|
||
var cfg = this.getCategoryCfg(geometry, attr, scale, legendOption);
|
||
return new CategoryLegend(cfg);
|
||
};
|
||
/**
|
||
* 获得连续图例的配置
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
* @param legendOption
|
||
*/
|
||
Legend.prototype.getContinuousCfg = function (geometry, attr, scale, legendOption) {
|
||
var ticks = scale.getTicks();
|
||
var containMin = find(ticks, function (tick) { return tick.value === 0; });
|
||
var containMax = find(ticks, function (tick) { return tick.value === 1; });
|
||
var items = ticks.map(function (tick) {
|
||
var value = tick.value, tickValue = tick.tickValue;
|
||
var attrValue = attr.mapping(scale.invert(value)).join('');
|
||
return {
|
||
value: tickValue,
|
||
attrValue: attrValue,
|
||
color: attrValue,
|
||
scaleValue: value,
|
||
};
|
||
});
|
||
if (!containMin) {
|
||
items.push({
|
||
value: scale.min,
|
||
attrValue: attr.mapping(scale.invert(0)).join(''),
|
||
color: attr.mapping(scale.invert(0)).join(''),
|
||
scaleValue: 0,
|
||
});
|
||
}
|
||
if (!containMax) {
|
||
items.push({
|
||
value: scale.max,
|
||
attrValue: attr.mapping(scale.invert(1)).join(''),
|
||
color: attr.mapping(scale.invert(1)).join(''),
|
||
scaleValue: 1,
|
||
});
|
||
}
|
||
// 排序
|
||
items.sort(function (a, b) { return a.value - b.value; });
|
||
// 跟 attr 相关的配置
|
||
// size color 区别的配置
|
||
var attrLegendCfg = {
|
||
min: head(items).value,
|
||
max: last(items).value,
|
||
colors: [],
|
||
rail: {
|
||
type: attr.type,
|
||
},
|
||
track: {},
|
||
};
|
||
if (attr.type === 'size') {
|
||
attrLegendCfg.track = {
|
||
style: {
|
||
// size 的选中前景色,对于 color,则直接使用 color 标识
|
||
// @ts-ignore
|
||
fill: attr.type === 'size' ? this.view.getTheme().defaultColor : undefined,
|
||
},
|
||
};
|
||
}
|
||
if (attr.type === 'color') {
|
||
attrLegendCfg.colors = items.map(function (item) { return item.attrValue; });
|
||
}
|
||
var container = this.container;
|
||
// if position is not set, use top as default
|
||
var direction = getDirection(legendOption);
|
||
var layout = getLegendLayout(direction);
|
||
var title = get(legendOption, 'title');
|
||
if (title) {
|
||
title = deepMix({
|
||
text: getName(scale),
|
||
}, title);
|
||
}
|
||
// 基础配置,从当前数据中读到的配置
|
||
attrLegendCfg.container = container;
|
||
attrLegendCfg.layout = layout;
|
||
attrLegendCfg.title = title;
|
||
attrLegendCfg.animateOption = DEFAULT_ANIMATE_CFG;
|
||
// @ts-ignore
|
||
return this.mergeLegendCfg(attrLegendCfg, legendOption, 'continuous');
|
||
};
|
||
/**
|
||
* 获取分类图例的配置项
|
||
* @param geometry
|
||
* @param attr
|
||
* @param scale
|
||
* @param custom
|
||
* @param legendOption
|
||
*/
|
||
Legend.prototype.getCategoryCfg = function (geometry, attr, scale, legendOption, custom) {
|
||
var container = this.container;
|
||
// if position is not set, use top as default
|
||
var direction = get(legendOption, 'position', DIRECTION.BOTTOM);
|
||
// the default marker style
|
||
var themeMarker = get(this.view.getTheme(), ['components', 'legend', direction, 'marker']);
|
||
var userMarker = get(legendOption, 'marker');
|
||
var layout = getLegendLayout(direction);
|
||
var items = custom ?
|
||
getCustomLegendItems(themeMarker, userMarker, legendOption.items) :
|
||
getLegendItems(this.view, geometry, attr, themeMarker, userMarker);
|
||
var title = get(legendOption, 'title');
|
||
if (title) {
|
||
title = deepMix({
|
||
text: scale ? getName(scale) : '',
|
||
}, title);
|
||
}
|
||
var baseCfg = this.getCategoryLegendSizeCfg(layout);
|
||
baseCfg.container = container;
|
||
baseCfg.layout = layout;
|
||
baseCfg.items = items;
|
||
baseCfg.title = title;
|
||
baseCfg.animateOption = DEFAULT_ANIMATE_CFG;
|
||
var categoryCfg = this.mergeLegendCfg(baseCfg, legendOption, direction);
|
||
if (categoryCfg.reversed) {
|
||
// 图例项需要逆序
|
||
categoryCfg.items.reverse();
|
||
}
|
||
return categoryCfg;
|
||
};
|
||
/**
|
||
* get legend config, use option > suggestion > theme
|
||
* @param baseCfg
|
||
* @param legendOption
|
||
* @param direction
|
||
*/
|
||
Legend.prototype.mergeLegendCfg = function (baseCfg, legendOption, direction) {
|
||
var themeObject = get(this.view.getTheme(), ['components', 'legend', direction], {});
|
||
return deepMix({}, themeObject, baseCfg, legendOption);
|
||
};
|
||
/**
|
||
* 生成 id
|
||
* @param key
|
||
*/
|
||
Legend.prototype.getId = function (key) {
|
||
return this.name + "-" + key;
|
||
};
|
||
/**
|
||
* 根据 id 来获取组件
|
||
* @param id
|
||
*/
|
||
Legend.prototype.getComponentById = function (id) {
|
||
return find(this.components, function (co) { return co.id === id; });
|
||
};
|
||
Legend.prototype.getCategoryLegendSizeCfg = function (layout) {
|
||
var _a = this.view.viewBBox, vw = _a.width, vh = _a.height;
|
||
var _b = this.view.coordinateBBox, cw = _b.width, ch = _b.height;
|
||
return layout === 'vertical'
|
||
? {
|
||
maxWidth: vw * COMPONENT_MAX_VIEW_PERCENTAGE,
|
||
maxHeight: ch,
|
||
}
|
||
: {
|
||
maxWidth: cw,
|
||
maxHeight: vh * COMPONENT_MAX_VIEW_PERCENTAGE,
|
||
};
|
||
};
|
||
return Legend;
|
||
}(Controller));
|
||
export default Legend;
|
||
//# sourceMappingURL=legend.js.map |