mirror of
https://gitee.com/antv/g6.git
synced 2024-12-15 18:11:08 +08:00
248 lines
6.8 KiB
JavaScript
248 lines
6.8 KiB
JavaScript
|
/**
|
||
|
* @fileOverview G6 Mapper Plugin base on d3 tech stack
|
||
|
* d3-scale https://github.com/d3/d3-scale
|
||
|
* d3-legend https://github.com/susielu/d3-legend
|
||
|
* @author huangtonger@aliyun.com
|
||
|
*/
|
||
|
const G6 = require('@antv/g6');
|
||
|
const d3 = require('d3');
|
||
|
const Util = G6.Util;
|
||
|
const DEFAULT_LEGEND_FILL = '#199CFB';
|
||
|
const DEFAULT_LEGEND_STROKE = '#199CFB';
|
||
|
const d3Legend = require('d3-svg-legend');
|
||
|
Util.mix(d3, d3Legend);
|
||
|
|
||
|
class Plugin {
|
||
|
constructor(itemType, dim, channel, range, otherCfg) {
|
||
|
Util.mix(this, {
|
||
|
/**
|
||
|
* 子项类型
|
||
|
* @type {String}
|
||
|
*/
|
||
|
itemType: null,
|
||
|
|
||
|
/**
|
||
|
* 数据纬度
|
||
|
* @type {String}
|
||
|
*/
|
||
|
dim: null,
|
||
|
|
||
|
/**
|
||
|
* 映射域
|
||
|
* @type {Array}
|
||
|
*/
|
||
|
range: null,
|
||
|
|
||
|
/**
|
||
|
* 视觉通道
|
||
|
* @type {String}
|
||
|
*/
|
||
|
channel: null,
|
||
|
|
||
|
/**
|
||
|
* 度量配置
|
||
|
* @type {object}
|
||
|
*/
|
||
|
scaleCfg: {},
|
||
|
|
||
|
/**
|
||
|
* 图例配置
|
||
|
* @type {object}
|
||
|
*/
|
||
|
legendCfg: {},
|
||
|
|
||
|
/**
|
||
|
* 是否数据对齐
|
||
|
* @type {boolean}
|
||
|
*/
|
||
|
nice: true
|
||
|
}, {
|
||
|
itemType,
|
||
|
dim,
|
||
|
channel,
|
||
|
range
|
||
|
}, otherCfg);
|
||
|
}
|
||
|
init() {
|
||
|
const graph = this.graph;
|
||
|
graph.on('beforechange', ev => {
|
||
|
if (ev.action === 'changeData') {
|
||
|
const { data } = ev;
|
||
|
this._createScale(graph.parseSource(data));
|
||
|
this._mapping();
|
||
|
this.legendCfg && this._createLegend();
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
_trainCategoryScale(itemType, data) {
|
||
|
const dim = this.dim;
|
||
|
const domainMap = {};
|
||
|
const domain = [];
|
||
|
data.forEach(model => {
|
||
|
if (!domainMap[model[dim]]) {
|
||
|
domainMap[model[dim]] = true;
|
||
|
domain.push(model[dim]);
|
||
|
}
|
||
|
});
|
||
|
return domain;
|
||
|
}
|
||
|
_trainNumberScale(itemType, data) {
|
||
|
const dim = this.dim;
|
||
|
const domain = [ Infinity, -Infinity ];
|
||
|
data.forEach(model => {
|
||
|
if (domain[0] > model[dim]) {
|
||
|
domain[0] = model[dim];
|
||
|
}
|
||
|
if (domain[1] < model[dim]) {
|
||
|
domain[1] = model[dim];
|
||
|
}
|
||
|
});
|
||
|
return domain;
|
||
|
}
|
||
|
_getScaleType(data) {
|
||
|
const dim = this.dim;
|
||
|
const scaleCfg = this.scaleCfg;
|
||
|
if (!scaleCfg.type) {
|
||
|
if (Util.isNumber(data[0][dim])) {
|
||
|
scaleCfg.type = 'linear';
|
||
|
} else {
|
||
|
scaleCfg.type = 'ordinal';
|
||
|
}
|
||
|
}
|
||
|
return Util.upperFirst(scaleCfg.type);
|
||
|
}
|
||
|
_createScale(sourceData) {
|
||
|
const itemType = this.itemType;
|
||
|
const data = sourceData[itemType + 's'];
|
||
|
const scaleType = this._getScaleType(data);
|
||
|
const scaleCfg = this.scaleCfg;
|
||
|
const nice = this.nice;
|
||
|
const range = this.range;
|
||
|
const scale = new d3['scale' + scaleType]();
|
||
|
|
||
|
let domain;
|
||
|
|
||
|
if (scaleType === 'Ordinal') {
|
||
|
domain = this._trainCategoryScale(itemType, data);
|
||
|
} else {
|
||
|
domain = this._trainNumberScale(itemType, data);
|
||
|
}
|
||
|
const rangeLength = range.length;
|
||
|
const domainLength = domain.length;
|
||
|
if (rangeLength !== domainLength) {
|
||
|
const domainStep = (domain[1] - domain[0]) / (rangeLength - 1);
|
||
|
range.forEach((v, i) => {
|
||
|
domain[i] = domain[0] + i * domainStep;
|
||
|
});
|
||
|
}
|
||
|
if (domain[0] === domain[1]) {
|
||
|
if (domain[0] > 0) {
|
||
|
domain[0] = 0;
|
||
|
} else if (domain[0] < 0) {
|
||
|
domain[1] = 0;
|
||
|
} else {
|
||
|
domain[0] = -1;
|
||
|
}
|
||
|
}
|
||
|
scale.range(range);
|
||
|
scale.domain(domain);
|
||
|
if (nice !== false && scale.nice) {
|
||
|
scale.nice();
|
||
|
}
|
||
|
Util.isFunction(scaleCfg.callBack) && scaleCfg.callBack(scale, domain);
|
||
|
this.scale = scale;
|
||
|
}
|
||
|
_getLegend() {
|
||
|
const channel = this.channel;
|
||
|
const scale = this.scale;
|
||
|
const itemType = this.itemType;
|
||
|
const shape = itemType === 'node' ? 'circle' : 'line';
|
||
|
const legendCfg = this.legendCfg;
|
||
|
const callback = legendCfg.callback;
|
||
|
const legend = d3['legend' + Util.upperFirst(channel)]();
|
||
|
if (channel === 'size') {
|
||
|
if (itemType === 'node') {
|
||
|
const sizeScale = scale.copy();
|
||
|
sizeScale.range([ 4, 16 ]);
|
||
|
legend.scale(sizeScale).shape(shape).shapePadding(8);
|
||
|
} else {
|
||
|
const sizeScale = scale.copy();
|
||
|
sizeScale.range([ 2, 16 ]);
|
||
|
legend.scale(sizeScale).shape(shape).shapePadding(8);
|
||
|
}
|
||
|
} else {
|
||
|
if (itemType === 'node') {
|
||
|
legend.scale(scale).shape(shape).shapePadding(8)
|
||
|
.shapeRadius(5);
|
||
|
} else {
|
||
|
legend.scale(scale).shape(shape);
|
||
|
}
|
||
|
}
|
||
|
Util.isFunction(callback) && callback(legend);
|
||
|
return legend;
|
||
|
}
|
||
|
_createLegend() {
|
||
|
const padding = 10;
|
||
|
const channel = this.channel;
|
||
|
const legendCfg = this.legendCfg;
|
||
|
const graph = this.graph;
|
||
|
const graphWidth = graph.getWidth();
|
||
|
const graphHeight = graph.getHeight();
|
||
|
const position = legendCfg.position ? legendCfg.position : 'br';
|
||
|
const graphContainer = graph.getGraphContainer();
|
||
|
const marginTop = legendCfg.marginTop ? legendCfg.marginTop : 0;
|
||
|
const marginLeft = legendCfg.marginLeft ? legendCfg.marginLeft : 0;
|
||
|
const marginBottom = legendCfg.marginBottom ? legendCfg.marginBottom : 0;
|
||
|
const marginRight = legendCfg.marginRight ? legendCfg.marginRight : 0;
|
||
|
const legend = this._getLegend();
|
||
|
const svg = d3.select(graphContainer)
|
||
|
.append('svg')
|
||
|
.style('position', 'absolute');
|
||
|
const legendCells = svg.call(legend).select('.legendCells');
|
||
|
const legendTitle = svg.call(legend).select('.legendTitle');
|
||
|
const swatchs = legendCells.selectAll('.swatch');
|
||
|
const node = legendCells.node();
|
||
|
const bbox = node.getBBox();
|
||
|
const tl = Util.getNineBoxPosition(position, {
|
||
|
x: 0,
|
||
|
y: 0,
|
||
|
width: graphWidth,
|
||
|
height: graphHeight
|
||
|
}, bbox.width + 2 * padding, bbox.height + 2 * padding, [ marginTop + padding, marginRight + padding, marginBottom + padding, marginLeft + padding ]);
|
||
|
if (legendTitle) {
|
||
|
legendTitle.attr('font-size', '14px');
|
||
|
}
|
||
|
if (channel === 'size') {
|
||
|
const legendFill = legendCfg.fill ? legendCfg.fill : DEFAULT_LEGEND_FILL;
|
||
|
const legendStroke = legendCfg.fill ? legendCfg.fill : DEFAULT_LEGEND_STROKE;
|
||
|
swatchs.attr('fill', legendFill);
|
||
|
swatchs.attr('stroke', legendStroke);
|
||
|
}
|
||
|
svg.style('overflow', 'visible')
|
||
|
.style('padding', padding + 'px')
|
||
|
.style('top', tl.y + 'px')
|
||
|
.style('left', tl.x + 'px')
|
||
|
.style('width', bbox.width + 'px')
|
||
|
.style('height', bbox.height + 'px');
|
||
|
}
|
||
|
_mapping() {
|
||
|
const graph = this.graph;
|
||
|
const dim = this.dim;
|
||
|
const itemType = this.itemType;
|
||
|
const scale = this.scale;
|
||
|
const channel = this.channel;
|
||
|
const mapper = {};
|
||
|
mapper[channel] = model => {
|
||
|
if (itemType === 'node' && channel === 'size') {
|
||
|
return scale(model[dim]);
|
||
|
}
|
||
|
return scale(model[dim]);
|
||
|
};
|
||
|
graph[itemType](mapper);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
G6.Plugins['tool.d3.mapper'] = Plugin;
|
||
|
|
||
|
module.exports = Plugin;
|