g6/plugins/tool.d3.mapper/index.js
2018-06-05 23:58:10 +08:00

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;