mirror of
https://gitee.com/antv/g6.git
synced 2024-12-14 01:21:33 +08:00
417 lines
12 KiB
JavaScript
417 lines
12 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 shiwu.wyy@antfin.com
|
|
*/
|
|
const G6 = require('@antv/g6');
|
|
const Legend = require('@antv/g2/src/component/legend');
|
|
const Color = require('@antv/g2/src/component/legend/color');
|
|
const Size = require('@antv/g2/src/component/legend/size');
|
|
const Attr = require('@antv/attr');
|
|
const Scale = require('@antv/scale');
|
|
const { Util, G } = G6;
|
|
|
|
class Plugin {
|
|
constructor(itemType, dim, channel, range, otherCfg) {
|
|
Util.mix(this, {
|
|
/**
|
|
* the type of the item, 'node'/'edge'
|
|
* @type {String}
|
|
*/
|
|
itemType: null,
|
|
|
|
/**
|
|
* 数据纬度
|
|
* @type {String}
|
|
*/
|
|
dim: null,
|
|
|
|
/**
|
|
* 映射域
|
|
* @type {Array}
|
|
*/
|
|
range: [ 0, 1 ],
|
|
|
|
/**
|
|
* 视觉通道
|
|
* @type {String}
|
|
*/
|
|
channel: null,
|
|
|
|
/**
|
|
* 度量配置
|
|
* @type {object}
|
|
*/
|
|
scaleCfg: {},
|
|
|
|
/**
|
|
* 图例配置
|
|
* @type {object}
|
|
*/
|
|
legendCfg: {
|
|
title: {
|
|
fill: '#333'
|
|
},
|
|
layout: 'horizontal' // horizontal or vertical
|
|
},
|
|
|
|
/**
|
|
* 是否数据对齐
|
|
* @type {boolean}
|
|
*/
|
|
nice: true,
|
|
curRange: [ 0, 100 ]
|
|
}, {
|
|
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(graph.parseSource(data));
|
|
}
|
|
});
|
|
}
|
|
_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 = 'category';
|
|
}
|
|
}
|
|
return Util.upperFirst(scaleCfg.type);
|
|
}
|
|
_scaleConsSelector(scaleType, scaleCfg) {
|
|
switch (scaleType) {
|
|
case 'Linear':
|
|
return new Scale.Linear(scaleCfg);
|
|
case 'Log':
|
|
return new Scale.Log(scaleCfg);
|
|
case 'Pow':
|
|
return new Scale.Pow(scaleCfg);
|
|
default:
|
|
return new Scale.Linear(scaleCfg);
|
|
}
|
|
}
|
|
_createScale(sourceData) {
|
|
const itemType = this.itemType;
|
|
const data = sourceData[itemType + 's'];
|
|
const scaleCfg = this.scaleCfg;
|
|
const scaleType = this._getScaleType(data);
|
|
const scale = this._scaleConsSelector(scaleType, scaleCfg);
|
|
const range = this.range;
|
|
scale.nice = scaleCfg.nice;
|
|
let domain = scaleCfg.domain;
|
|
scale.range = range;
|
|
if (!domain) {
|
|
if (scaleType === 'Category') {
|
|
domain = this._trainCategoryScale(itemType, data);
|
|
} else {
|
|
domain = this._trainNumberScale(itemType, data);
|
|
}
|
|
}
|
|
const domainLength = domain.length;
|
|
if (domainLength === 2 && domain[0] === domain[1]) {
|
|
if (domain[0] > 0) {
|
|
domain[0] = 0;
|
|
} else if (domain[0] < 0) {
|
|
domain[1] = 0;
|
|
} else {
|
|
domain[0] = -1;
|
|
}
|
|
}
|
|
scale.values = domain;
|
|
scale.min = domain[0];
|
|
scale.max = domain[domain.length - 1];
|
|
Util.isFunction(scaleCfg.callback) && scaleCfg.callback(scale, domain);
|
|
this.scale = scale;
|
|
|
|
}
|
|
_createLegend(sourceData) {
|
|
const itemType = this.itemType;
|
|
const data = sourceData[itemType + 's'];
|
|
const scaleType = this._getScaleType(data);
|
|
const channel = this.channel;
|
|
const graph = this.graph;
|
|
|
|
const containerId = this.legendCfg.containerId;
|
|
let legendContainer = this.legendCfg.container;
|
|
if (legendContainer === undefined) {
|
|
if (containerId === undefined) {
|
|
legendContainer = Util.createDOM('<div class="legend-container"></div>');
|
|
const container = graph.getGraphContainer();
|
|
container.appendChild(legendContainer);
|
|
} else {
|
|
legendContainer = document.getElementById(containerId);
|
|
if (legendContainer === undefined || legendContainer === null) {
|
|
throw new Error('please set the container for the graph !');
|
|
}
|
|
}
|
|
}
|
|
const canvas = new G.Canvas({
|
|
containerId, // dom_id,
|
|
containerDOM: legendContainer,
|
|
width: 500,
|
|
height: 500
|
|
});
|
|
let legend;
|
|
const domain = this.scale.values;
|
|
const dim = this.dim;
|
|
if (scaleType === 'Category') {
|
|
legend = this._createCatLegend(canvas);
|
|
// the listener for category legend to filter nodes and edges
|
|
graph.addFilter(item => {
|
|
if (this.curRange[0] === 0 && this.curRange[1] === 100) {
|
|
return true;
|
|
}
|
|
if (item.isNode) {
|
|
const val = item.model[dim];
|
|
let visible = true;
|
|
Util.each(this.curRange, r => {
|
|
const checked = r.get('checked');
|
|
const name = r.get('value');
|
|
if (!checked && name === val) {
|
|
visible = false;
|
|
}
|
|
});
|
|
return visible;
|
|
} else if (item.isEdge) {
|
|
const sourceVal = item.source.model[dim];
|
|
let sourceVisible = true;
|
|
const targetVal = item.target.model[dim];
|
|
let targetVisible = true;
|
|
Util.each(this.curRange, r => {
|
|
const checked = r.get('checked');
|
|
const name = r.get('value');
|
|
if (!checked && name === sourceVal) {
|
|
sourceVisible = false;
|
|
}
|
|
if (!checked && name === targetVal) {
|
|
targetVisible = false;
|
|
}
|
|
});
|
|
if (!sourceVisible || !targetVisible) return false;
|
|
return true;
|
|
}
|
|
});
|
|
legend.on('click', () => {
|
|
const itemsGroup = legend.get('itemsGroup');
|
|
this.curRange = itemsGroup.get('children');
|
|
graph.filter();
|
|
legend.get('canvas').draw();
|
|
});
|
|
} else {
|
|
if (channel === 'color') {
|
|
legend = this._createContinuousColorLegend(canvas);
|
|
} else {
|
|
legend = this._createContinuousSizeLegend(canvas);
|
|
}
|
|
// the listener to filter nodes and edges
|
|
const slider = legend.get('slider');
|
|
graph.addFilter(item => {
|
|
if (item.isNode) {
|
|
const val = item.model[dim];
|
|
const percent = 100 * (val - domain[0]) / (domain[domain.length - 1] - domain[0]);
|
|
if (percent > this.curRange[1] || percent < this.curRange[0]) {
|
|
return false;
|
|
}
|
|
return true;
|
|
} else if (item.isEdge) {
|
|
const sourceVal = item.source.model[dim];
|
|
const sourcePercent = 100 * (sourceVal - domain[0]) / (domain[domain.length - 1] - domain[0]);
|
|
const sourceVisible = (sourcePercent <= this.curRange[1] && sourcePercent >= this.curRange[0]);
|
|
const targetVal = item.target.model[dim];
|
|
const targetPercent = 100 * (targetVal - domain[0]) / (domain[domain.length - 1] - domain[0]);
|
|
const targetVisible = (targetPercent <= this.curRange[1] && targetPercent >= this.curRange[0]);
|
|
if (!sourceVisible || !targetVisible) return false;
|
|
return true;
|
|
}
|
|
});
|
|
slider.on('sliderchange', Util.throttle(ev => {
|
|
this.curRange = ev.range;
|
|
graph.filter();
|
|
}, 100));
|
|
}
|
|
const bbox = legend.getBBox();
|
|
const padding = 6;
|
|
const legendWidth = bbox.maxX - bbox.minX;
|
|
const legendHeight = bbox.maxY - bbox.minY;
|
|
legend.move(-bbox.minX + padding, -bbox.minY + padding);
|
|
canvas.changeSize(legendWidth + 2 * padding, legendHeight + 2 * padding);
|
|
this.legend = legend;
|
|
this.legendCanvas = canvas;
|
|
this.legendWidth = legendWidth;
|
|
this.legendHeight = legendHeight;
|
|
canvas.draw();
|
|
}
|
|
_createCatLegend(canvas) {
|
|
const scale = this.scale;
|
|
const range = scale.range;
|
|
const domain = scale.values;
|
|
const items = [];
|
|
Util.each(range, (val, i) => {
|
|
items.push({
|
|
value: domain[i], // marker text
|
|
marker: {
|
|
symbol: 'circle',
|
|
radius: 5,
|
|
fill: val // marker color
|
|
},
|
|
checked: true
|
|
});
|
|
});
|
|
const cfg = Util.mix({
|
|
items,
|
|
checkable: true,
|
|
clickable: true
|
|
}, this.legendCfg);
|
|
const legend = canvas.addGroup(Legend.Category, cfg);
|
|
return legend;
|
|
}
|
|
_getLegendCfg(defaultCfg) {
|
|
const legendCfg = this.legendCfg;
|
|
if (Util.isNil(legendCfg.width)) {
|
|
if (legendCfg.layout === 'horizontal') {
|
|
legendCfg.width = 150;
|
|
legendCfg.height = 15;
|
|
} else {
|
|
legendCfg.width = 15;
|
|
legendCfg.height = 150;
|
|
}
|
|
}
|
|
return Util.mix(true, {}, defaultCfg, legendCfg);
|
|
}
|
|
_createContinuousColorLegend(canvas) {
|
|
const scale = this.scale;
|
|
const range = scale.range;
|
|
const domain = scale.values;
|
|
const domainStep = (domain[domain.length - 1] - domain[0]) / (range.length - 1);
|
|
const items = [];
|
|
Util.each(range, (val, i) => {
|
|
const itemText = domain[0] + domainStep * i;
|
|
const percent = (itemText - domain[0]) / (domain[domain.length - 1] - domain[0]);
|
|
items.push({
|
|
attrValue: val,
|
|
value: itemText, // the number label of the slider
|
|
scaleValue: percent
|
|
});
|
|
});
|
|
const cfg = this._getLegendCfg({
|
|
items,
|
|
title: {
|
|
text: this.dim
|
|
}
|
|
});
|
|
const legend = canvas.addGroup(Color, cfg);
|
|
return legend;
|
|
}
|
|
_createContinuousSizeLegend(canvas) {
|
|
const scale = this.scale;
|
|
const range = scale.range;
|
|
const domain = scale.values;
|
|
const domainStep = (domain[domain.length - 1] - domain[0]) / (range.length - 1);
|
|
|
|
const items = [];
|
|
Util.each(range, (val, i) => {
|
|
const dom = domain[0] + domainStep * i;
|
|
items.push({
|
|
attrValue: val,
|
|
value: dom // the number label of the slider
|
|
});
|
|
});
|
|
const cfg = this._getLegendCfg({
|
|
items,
|
|
attrType: 'size',
|
|
title: {
|
|
text: this.dim
|
|
}
|
|
});
|
|
const legend = canvas.addGroup(Size, cfg);
|
|
return legend;
|
|
}
|
|
_mapping() {
|
|
const graph = this.graph;
|
|
const dim = this.dim;
|
|
const itemType = this.itemType;
|
|
const scale = this.scale;
|
|
const channel = this.channel;
|
|
|
|
const domain = scale.values;
|
|
const range = scale.range;
|
|
let color;
|
|
if (channel === 'color') {
|
|
const colorScale = this._scaleSelector(Util.upperFirst(scale.type), domain);
|
|
color = new Attr.Color({
|
|
scales: [ colorScale ],
|
|
values: range
|
|
});
|
|
}
|
|
graph[itemType]()[channel](model => {
|
|
if (itemType === 'node' && channel === 'size') {
|
|
return scale.scale(model[dim]);
|
|
} else if (channel === 'color') {
|
|
return color.mapping(model[dim])[0];
|
|
}
|
|
return scale.scale(model[dim]);
|
|
});
|
|
}
|
|
_scaleSelector(type, domain) {
|
|
const params = {
|
|
min: domain[0],
|
|
max: domain[domain.length - 1]
|
|
};
|
|
switch (type) {
|
|
case 'Category':
|
|
return Scale.cat({
|
|
values: domain
|
|
});
|
|
default:
|
|
return Scale.linear(params);
|
|
}
|
|
}
|
|
destroy() {
|
|
this.legendCanvas && this.legendCanvas.destroy();
|
|
}
|
|
}
|
|
|
|
G6.Plugins['tool.mapper'] = Plugin;
|
|
module.exports = Plugin;
|