@antv/g6
Version:
graph visualization frame work
835 lines (807 loc) • 20.8 kB
JavaScript
/**
* @fileOverview graph
* @author huangtonger@aliyun.com
*/
require('./extend/g/html');
require('./extend/g/canvas');
require('./extend/g/group');
require('./extend/g/shape');
require('./extend/g/root-group');
require('./extend/g/html');
const Base = require('./base');
const Item = require('./item/');
const Shape = require('./shape/');
const Util = require('./util/');
const G = require('@antv/g');
const LayoutMixin = require('./mixin/layout');
const MappingMixin = require('./mixin/mapping');
const QueryMixin = require('./mixin/query');
const EventMixin = require('./mixin/event');
const ModeMixin = require('./mixin/mode');
const FilterMixin = require('./mixin/filter');
const AnimateMixin = require('./mixin/animate');
const FitView = require('./mixin/fit-view');
const ForceFit = require('./mixin/force-fit');
const Mixins = [ FilterMixin, MappingMixin, QueryMixin, AnimateMixin, ForceFit, LayoutMixin, FitView, EventMixin, ModeMixin ];
const TAB_INDEX = 20;
class Graph extends Base {
/**
* Access to the default configuration properties
* @return {object} default configuration
*/
getDefaultCfg() {
return {
/**
* Container could be dom object or dom id
* @type {object|string|undefined}
*/
container: undefined,
/**
* Canvas width
* @type {number|undefined}
* unit pixel if undefined force fit width
*/
width: undefined,
/**
* Canvas height
* @type {number|undefined}
* unit pixel if undefined force fit height
*/
height: undefined,
/**
* Plugins
* @type {array}
*/
plugins: [],
/**
* FontFamily
* @type {string}
*/
fontFamily: '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", SimSun, "sans-serif"',
/**
* default node shape
* @type {string|undefined}
*/
nodeDefaultShape: undefined,
/**
* default edge shape
* @type {string|undefined}
*/
edgeDefaultShape: undefined,
/**
* default group shape
* @type {string|undefined}
*/
groupDefaultShape: undefined,
/**
* default edge node intersect box
* @type {string}
*/
defaultIntersectBox: 'circle',
render: 'canvas',
_controllers: {},
_timers: {},
_dataMap: {},
_itemMap: {},
_data: {},
_delayRunObj: {}
};
}
constructor(inputCfg) {
const cfg = {};
Mixins.forEach(Mixin => {
Util.mix(cfg, Mixin.CFG, inputCfg);
});
super(cfg);
// plugin should init before all
this._pluginInit();
this.emit('beforeinit');
this._init();
this.emit('afterinit');
}
_init() {
this._initData();
this._initContainer();
this._initCanvas();
Mixins.forEach(Mixin => {
Mixin.INIT && this[Mixin.INIT]();
});
this.initEvent();
}
initEvent() {
}
_executeLayout(processer, nodes, edges, groups) {
if (Util.isFunction(processer)) {
processer(nodes, edges, this);
} else if (Util.isObject(processer)) {
processer.nodes = nodes;
processer.edges = edges;
processer.groups = groups;
processer.graph = this;
processer.execute();
}
}
_pluginInit() {
const plugins = this.get('plugins');
plugins.forEach(plugin => {
this._initPlugin(plugin);
});
}
_initPlugin(plugin) {
plugin.graph = this;
plugin.init && plugin.init();
}
_getTimer(name) {
return this.get('_timers')[name];
}
_setTimer(name, value) {
this.get('_timers')[name] = value;
}
_getController(name) {
return this.get('_controllers')[name];
}
_initContainer() {
let container = this.get('container');
if (!container) {
// Compatible with id written
container = this.get('id');
}
if (container) {
if (Util.isString(container)) {
container = document.getElementById(container);
}
} else {
throw new Error('please set the container for the graph !');
}
const graphContainer = Util.createDOM('<div class="graph-container"></div>', {
position: 'relative'
});
container.appendChild(graphContainer);
this.set('_containerDOM', container);
this.set('_graphContainer', graphContainer);
}
_initCanvas() {
const graphContainer = this.get('_graphContainer');
const width = this.get('width');
const height = this.get('height');
const fontFamily = this.get('fontFamily');
const canvasCfg = {
width,
height,
fontFamily,
eventEnable: false,
containerDOM: graphContainer
};
const Canvas = this.getConstructor('Canvas');
const canvas = new Canvas(canvasCfg);
const frontCanvas = new Canvas(canvasCfg);
const frontEl = frontCanvas.get('el');
const htmlElementContaniner = graphContainer.appendChild(Util.createDOM('<div class="graph-container-html-Elements"></div>'));
canvas.on('beforedraw', () => {
this.emit('beforecanvasdraw');
});
frontEl.style.position = 'absolute';
frontEl.style.top = 0;
frontEl.style.left = 0;
htmlElementContaniner.style.overflow = 'hidden';
htmlElementContaniner.style.width = width + 'px';
htmlElementContaniner.style.height = height + 'px';
htmlElementContaniner.style.position = 'absolute';
htmlElementContaniner.style.top = 0;
htmlElementContaniner.style.left = 0;
this.set('_canvas', canvas);
this.set('_frontCanvas', frontCanvas);
this.set('_htmlElementContaniner', htmlElementContaniner);
const mouseEventWrapper = this.getMouseEventWrapper();
mouseEventWrapper.style.outline = 'none';
mouseEventWrapper.style['user-select'] = 'none';
mouseEventWrapper.setAttribute('tabindex', TAB_INDEX);
canvas.set('htmlElementContaniner', htmlElementContaniner);
const RootGroup = this.getConstructor('RootGroup');
const rootGroup = canvas.addGroup(RootGroup);
const frontRootGroup = frontCanvas.addGroup(RootGroup);
const itemGroup = rootGroup.addGroup();
this.set('_itemGroup', itemGroup);
this.set('_rootGroup', rootGroup);
this.set('_frontRootGroup', frontRootGroup);
}
_initData() {
this.set('_dataMap', {});
this.set('_itemMap', {
_nodes: [],
_edges: [],
_groups: [],
_guides: []
});
this.set('_data', {});
}
_refresh() {
}
getKeyboardEventWrapper() {
const keyboardEventWrapper = this.get('keyboardEventWrapper');
return keyboardEventWrapper ? keyboardEventWrapper : this.getMouseEventWrapper();
}
getMouseEventWrapper() {
return this.get('_htmlElementContaniner');
}
/**
* @param {object} plugin - plugin instance
*/
addPlugin(plugin) {
const plugins = this.get('plugins');
this._initPlugin(plugin);
plugins.push(plugin);
}
/**
* @return {domobject} graphcontainer
*/
getGraphContainer() {
return this.get('_graphContainer');
}
/**
* @param {string} type item type
* @param {array} models models
*/
addItems(type, models) {
this._addDatas(type, models);
const Type = Util.upperFirst(type);
const Constructor = Item[Type];
const itemMap = this.get('_itemMap');
const itemGroup = this.get('_itemGroup');
const dataMap = this.get('_dataMap');
const animate = this.get('animate');
const defaultIntersectBox = this.get('defaultIntersectBox');
if (!Constructor) {
throw new Error('please set valid item type!');
}
models.forEach(model => {
const item = new Constructor({
id: model.id,
type,
model,
group: itemGroup.addGroup(),
graph: this,
mapper: this._getController(type + 'Mapper'),
itemMap,
animate,
dataMap,
defaultIntersectBox
});
itemMap[model.id] = item;
itemMap['_' + type + 's'].push(item);
});
}
/**
* @param {array} items remove items
*/
removeItems(items) {
const dataMap = this.get('_dataMap');
const itemMap = this.get('_itemMap');
items.forEach(item => {
delete dataMap[item.id];
delete itemMap[item.id];
Util.Array.remove(itemMap['_' + item.type + 's'], item);
item.destroy();
});
}
/**
* @param {object} item item
* @param {object} model update model
*/
updateItem(item, model) {
Util.mix(item.getModel(), model);
item.update();
}
_addDatas(type, models) {
const dataMap = this.get('_dataMap');
models.forEach(model => {
if (Util.isNil(model.id)) {
model.id = Util.guid();
}
if (dataMap[model.id]) {
throw new Error('id:' + model.id + ' has already been set, please set new one');
}
dataMap[model.id] = model;
});
}
_drawInner() {
const data = this.get('_data');
const itemGroup = this.get('_itemGroup');
const dataMap = this.get('_dataMap');
if (data.nodes) {
this.addItems('node', data.nodes);
}
if (data.groups) {
this.addItems('group', data.groups);
}
if (data.edges) {
this.addItems('edge', data.edges);
}
if (data.guides) {
this.addItems('guide', data.guides);
}
itemGroup.sortBy(child => {
const id = child.id;
const model = dataMap[id];
return model.index;
});
}
_clearInner() {
const items = this.getItems();
items.forEach(item => {
item && !item.destroyed && item.destroy();
});
}
/**
* @param {string} name option 1
* @return {function} function
*/
getConstructor(name) {
const render = this.get('render');
if (render === 'svg') {
return G.svg[name];
}
return G.canvas[name];
}
/**
* @param {string} type item type
* @param {object} model data model
* @return {object} shapeObj
*/
getShapeObj(type, model) {
if (!Util.isObject(type)) {
const Type = Util.upperFirst(type);
const shapeManager = Shape[Type];
const defaultShape = this.get(type + 'DefaultShape');
return shapeManager.getShape(model.shape, defaultShape);
}
return type.getShapeObj();
}
/**
* @return {object} source data
*/
getSource() {
return this.get('_sourceData');
}
/**
* @param {object} data source data
* @return {object} plain data
*/
parseSource(data) {
return data;
}
/**
* @return {G.Canvas} canvas
*/
getCanvas() {
return this.get('_canvas');
}
/**
* @return {G.Group} rootGroup
*/
getRootGroup() {
return this.get('_rootGroup');
}
/**
* @return {G.Group} itemGroup
*/
getItemGroup() {
return this.get('_itemGroup');
}
/**
* @return {G.Group} frontRootGroup
*/
getFrontRootGroup() {
return this.get('_frontRootGroup');
}
/**
* @return {G.Canvas} canvas
*/
getFrontCanvas() {
return this.get('_frontCanvas');
}
/**
* @param {object} data source data
* @return {Graph} this
*/
source(data) {
this.emit('beforesource');
this.set('_data', data);
this.set('_sourceData', data);
this.emit('aftersource');
return this;
}
/**
* @return {Graph} this
*/
render() {
this.emit('beforerender');
this.emit('beforedrawinner');
this._drawInner();
this.emit('afterdrawinner');
this.draw();
this.emit('afterrender');
return this;
}
/**
* @param {boolean} bool if force prevent animate
*/
forcePreventAnimate(bool) {
this.set('forcePreventAnimate', bool);
}
/**
* @return {Graph} this
*/
reRender() {
const data = this.get('_sourceData');
this.read(data);
return this;
}
/**
* @return {Graph} this
*/
destroy() {
const canvas = this.get('_canvas');
const frontCanvas = this.get('_frontCanvas');
const graphContainer = this.get('_graphContainer');
const controllers = this.get('_controllers');
const timers = this.get('_timers');
const windowForceResizeEvent = this.get('_windowForceResizeEvent');
const plugins = this.get('plugins');
Util.each(timers, timer => {
clearTimeout(timer);
});
Util.each(controllers, controller => {
controller.destroy();
});
plugins.forEach(plugin => {
plugin.destroy && plugin.destroy();
});
canvas && canvas.destroy();
frontCanvas && frontCanvas.destroy();
graphContainer.destroy();
window.removeEventListener('resize', windowForceResizeEvent);
super.destroy();
return this;
}
/**
* @return {object} data
*/
save() {
const itemGroup = this.get('_itemGroup');
const children = itemGroup.get('children');
const rst = {
nodes: [],
edges: [],
groups: [],
guides: []
};
children.forEach((child, index) => {
const model = child.model;
if (model) {
const type = child.itemType;
const saveModel = Util.cloneDeep(model);
saveModel.index = index;
rst[type + 's'].push(saveModel);
}
});
rst.nodes.length === 0 && delete rst.nodes;
rst.edges.length === 0 && delete rst.edges;
rst.groups.length === 0 && delete rst.groups;
rst.guides.length === 0 && delete rst.guides;
return rst;
}
/**
* @param {string} type item type
* @param {object} model data model
* @return {Graph} this
*/
add(type, model) {
const ev = {
action: 'add',
model
};
this.emit('beforechange', ev);
const itemMap = this.get('_itemMap');
this.addItems(type, [ model ]);
const item = itemMap[model.id];
item.getAllParents().forEach(parent => {
parent.update();
});
ev.item = item;
this.emit('afterchange', ev);
this.draw();
return item;
}
/**
* @param {String|Item} item target item
* @return {Graph} this
*/
remove(item) {
item = this.getItem(item);
if (!item || item.destroyed) {
return;
}
const ev = {
action: 'remove',
item
};
this.emit('beforechange', ev);
if (item.isNode || item.isGroup) {
const edges = item.getEdges();
edges.forEach(edge => {
this.remove(edge);
});
}
if (item.isGroup) {
const children = item.getChildren();
children.forEach(child => {
this.remove(child);
});
}
this.removeItems([ item ]);
item.getAllParents().forEach(parent => {
parent.update();
});
this.emit('afterchange', ev);
this.draw();
return this;
}
/**
* @param {String|Item} item target item
* @param {object} model data model
* @return {Graph} this
*/
simpleUpdate(item, model) {
this.updateItem(item, model);
this.draw();
return this;
}
/**
* @param {String|Item|Undefined} item target item
* @param {object} model data model
* @return {Graph} this
*/
update(item, model) {
const itemMap = this.get('_itemMap');
item = this.getItem(item);
if (!item || item.destroyed) {
return;
}
const itemModel = item.getModel();
const originModel = Util.mix({
}, itemModel);
const ev = {
action: 'update',
item,
originModel,
updateModel: model
};
const originParent = itemMap[originModel.parent];
if (originParent && (originParent !== parent) && Util.isGroup(originParent)) {
item.getAllParents().forEach(parent => {
parent.update();
});
}
model && this.emit('beforechange', ev);
this.updateItem(item, model);
// If the update nodes or group, update their parent
item.getAllParents().forEach(parent => {
parent.update();
});
// If the update nodes or group, update the connection edge
if ((item.isNode || item.isGroup) && !item.collapsedParent) {
const edges = item.getEdges();
edges.forEach(edge => {
edge.update();
});
}
// update group relative items
if (item.isGroup && model) {
item.deepEach(child => {
child.updateCollapsedParent();
if (child.collapsedParent) {
child.hide();
} else {
child.show();
}
child.update();
});
item.getInnerEdges().forEach(child => {
const bool = child.linkedItemVisible();
if (bool) {
child.show();
} else {
child.hide();
}
child.update();
});
}
model && this.emit('afterchange', ev);
this.draw();
return this;
}
/**
* change data
* @param {object} data source data
* @return {Graph} this
*/
read(data) {
if (!data) {
throw new Error('please read valid data!');
}
const fitView = this.get('fitView');
const ev = {
action: 'changeData',
data
};
this.emit('beforechange', ev);
this.clear();
this.source(data);
this.render();
this.emit('afterchange', ev);
fitView && this.setFitView(fitView);
return this;
}
/**
* @return {Graph} this
*/
clear() {
this.emit('beforeclear');
this._clearInner();
this._initData();
this.emit('afterclear');
this.draw();
return this;
}
/**
* hide item
* @param {number} item input item
* @return {object} this
*/
hide(item) {
item = this.getItem(item);
item.hide();
if (item.isNode) {
item.getEdges().forEach(edge => {
edge.hide();
});
}
if (item.isGroup) {
item.getEdges().forEach(edge => {
edge.hide();
});
// item.getInnerEdges().forEach(edge => {
// edge.hide();
// });
item.deepEach(child => {
child.hide();
});
}
this.draw();
return this;
}
_tryShowEdge(edge) {
const source = edge.getSource();
const target = edge.getTarget();
return (source.linkable && source.isVisible() || !source.linkable)
&& (target.linkable && target.isVisible() || !target.linkable)
&& edge.show();
}
/**
* show item
* @param {number} item input item
* @return {object} this
*/
show(item) {
item = this.getItem(item);
if (item.isEdge) {
this._tryShowEdge(item);
} else {
item.show();
}
if (item.isNode) {
item.getEdges().forEach(edge => {
this._tryShowEdge(edge);
});
}
if (item.isGroup) {
item.getEdges().forEach(edge => {
this._tryShowEdge(edge);
});
item.deepEach(child => {
child.show();
});
}
this.draw();
return this;
}
/**
* @return {Graph} this
*/
getWidth() {
return this.get('width');
}
/**
* @return {Graph} this
*/
getHeight() {
return this.get('height');
}
/**
* change canvas size
* @param {number} width input width
* @param {number} height input height
* @return {object} this
*/
changeSize(width, height) {
if (Math.abs(width) >= Infinity || Math.abs(height) >= Infinity) {
console.warn('size parameter more than the maximum');
return;
}
const canvas = this.get('_canvas');
const frontCanvas = this.get('_frontCanvas');
const htmlElementContaniner = this.get('_htmlElementContaniner');
if (width !== this.get('width') || height !== this.get('height')) {
this.emit('beforechangesize');
canvas.changeSize(width, height);
frontCanvas.changeSize(width, height);
htmlElementContaniner.css({
width: width + 'px',
height: height + 'px'
});
this.set('width', width);
this.set('height', height);
this.emit('afterchangesize');
this.draw();
}
return this;
}
/**
* item to front
* @param {object} item item
*/
toFront(item) {
item = this.getItem(item);
const itemGroup = this.get('_itemGroup');
const group = item.getGraphicGroup();
Util.toFront(group, itemGroup);
this.draw();
}
/**
* item to back
* @param {object} item item
*/
toBack(item) {
item = this.getItem(item);
const itemGroup = this.get('_itemGroup');
const group = item.getGraphicGroup();
Util.toBack(group, itemGroup);
this.draw();
}
/**
* set cantainer css
* @param {object} style container dom css
*/
css(style) {
const graphContainer = this.getGraphContainer();
Util.modifyCSS(graphContainer, style);
}
/**
* save graph image
* @return {object} canvas dom
*/
saveImage() {
const box = this.getBBox();
const padding = this.getFitViewPadding();
return Util.graph2Canvas({
graph: this,
width: box.width + padding[1] + padding[3],
height: box.height + padding[0] + padding[2]
});
}
}
Mixins.forEach(Mixin => {
Util.mix(Graph.prototype, Mixin.AUGMENT);
});
module.exports = Graph;