ソースを参照

feat(plugin): add plugin

yilin.qyl 6 年 前
コミット
a8a93c398d

+ 5 - 5
demos/node-animate.html

@@ -25,7 +25,7 @@
 	    x: 300,
 	    y: 200,
 	    shape: 'background-animate',
-	    color: 'red',
+	    color: '#b5b5b5',
 	    size: 20,
 	   	label: '背景动画',
 	   	labelCfg: {
@@ -66,7 +66,7 @@
 	    target: 'node4'
 	  }
 	]};
-	
+
 	// 放大、变小动画
 	G6.registerNode('circle-animate', {
 		afterDraw(cfg, group) {
@@ -102,7 +102,7 @@
 					x: 0,
 					y: 0,
 					r,
-					fill: 'blue', // 为了显示清晰,随意设置了颜色
+					fill: cfg.color, // 为了显示清晰,随意设置了颜色
 					opacity: 0.6
 				}
 			});
@@ -113,7 +113,7 @@
 					x: 0,
 					y: 0,
 					r,
-					fill: 'green',
+					fill: cfg.color,
 					opacity: 0.6
 				}
 			});
@@ -176,4 +176,4 @@
   graph.render();
 </script>
 </body>
-</html>
+</html>

+ 2 - 2
demos/sankey-diagram.html

@@ -11,8 +11,8 @@
 <script src="./assets/sankey.js"></script>
 <script src="./assets/jquery-3.2.1.min.js"></script>
 <script>
-  const width = 960;
-  const height = 500;
+  const width = window.innerWidth;
+  const height = window.innerHeight;
   const colors = ['#FD8C3D', '#D83F43', '#F7BED6', '#E487C7', '#46A848', '#D83F43', '#3B85BA', '#48335B', '#B7CDE9'];
 
   G6.registerEdge('sankey', {

+ 10 - 1
demos/setState.html

@@ -18,18 +18,27 @@
 <body>
 <div id="mountNode"></div>
 <script src="../build/g6.js"></script>
+<script src="../build/minimap.js"></script>
 <script src="./assets/d3-4.13.0.min.js"></script>
 <script src="./assets/jquery-3.2.1.min.js"></script>
 <script>
+  const minimap = new Minimap({ size: [ 200, 160 ] });
   const graph = new G6.Graph({
     container: 'mountNode',
     width: 1000,
     height: 800,
     autoPaint: false,
+    plugins: [ minimap ],
     modes: {
       default: ['drag-canvas', {
         type: 'tooltip',
         formatText: model => { return  model.name}
+      }, {
+        type: 'edge-tooltip',
+        formatText: (model, e) => {
+          const edge = e.item;
+          return '来源:' + edge.getSource().getModel().name + '<br/>去向:' + edge.getTarget().getModel().name;
+        }
       }]
     },
     defaultNode: {
@@ -53,7 +62,7 @@
       }
     },
     edgeStyle: {
-      default: { stroke: '#e2e2e2' },
+      default: { stroke: '#e2e2e2', lineAppendWidth: 2 },
       highlight: { stroke: '#999' }
     }
   });

+ 44 - 0
plugins/base.js

@@ -0,0 +1,44 @@
+const deepMix = require('@antv/util/lib/deep-mix');
+const each = require('@antv/util/lib/each');
+const wrapBehavior = require('@antv/util/lib/event/wrap-behavior');
+
+class PluginBase {
+  constructor(cfgs) {
+    this._cfgs = deepMix(this.getDefaultCfg(), cfgs);
+  }
+  getDefaultCfg() {
+    return {};
+  }
+  init(graph) {
+    const self = this;
+    self.set('graph', graph);
+    const events = self.getEvents();
+    const bindEvents = [];
+    each(events, (v, k) => {
+      const event = wrapBehavior(self, v);
+      bindEvents[k] = event;
+      graph.on(k, event);
+    });
+    this._events = bindEvents;
+  }
+  getEvents() {
+    return {};
+  }
+  get(key) {
+    return this._cfgs[key];
+  }
+  set(key, val) {
+    this._cfgs[key] = val;
+  }
+  destroy() {
+    const graph = this.get('graph');
+    each(this._events, (v, k) => {
+      graph.off(k, v);
+    });
+    this._events = null;
+    this._cfgs = null;
+    this.destroyed = true;
+  }
+}
+
+module.exports = PluginBase;

+ 5 - 0
plugins/index.js

@@ -0,0 +1,5 @@
+const G6Plugins = {
+  minimap: require('./minimap')
+};
+
+module.exports = G6Plugins;

+ 242 - 0
plugins/minimap/index.js

@@ -0,0 +1,242 @@
+const G = require('@antv/g/lib');
+const Base = require('../base');
+const isString = require('@antv/util/lib/type/is-string');
+const isNil = require('@antv/util/lib/type/is-nil');
+const createDOM = require('@antv/util/lib/dom/create-dom');
+const modifyCSS = require('@antv/util/lib/dom/modify-css');
+const each = require('@antv/util/lib/each');
+
+const max = Math.max;
+
+class Minimap extends Base {
+  init(graph) {
+    super.init(graph);
+    this.initContainer();
+  }
+  getDefaultCfg() {
+    return {
+      container: null,
+      className: 'g6-minimap',
+      viewportClassName: 'g6-minimap-viewport',
+      keyShapeOnly: false,
+      viewportStyle: {
+        stroke: '#1890ff',
+        lineWidth: 2,
+        x: 0,
+        y: 0,
+        width: 200,
+        height: 120
+      },
+      size: [ 200, 120 ]
+    };
+  }
+  getEvents() {
+    return { beforepaint: 'updateCanvas' };
+  }
+  initContainer() {
+    const self = this;
+    const graph = self.get('graph');
+    const size = self.get('size');
+    const className = self.get('className');
+    let container = self.get('container');
+    if (isString(container)) {
+      container = document.getElementById(container);
+    }
+    if (container) {
+      container.classList.add(className);
+      modifyCSS(container, {
+        width: size[0] + 'px',
+        height: size[1] + 'px'
+      });
+    } else {
+      container = createDOM('<div class="' + className + '" style="width:' + size[0] + 'px; height:' + size[1] + 'px"></div>');
+      graph.get('container').appendChild(container);
+    }
+    self.set('container', container);
+    const containerDOM = createDOM('<div class="g6-minimap-container"></div>');
+    container.appendChild(containerDOM);
+    const canvas = new G.Canvas({
+      containerDOM,
+      width: size[0],
+      height: size[1],
+      pixelRatio: graph.get('pixelRatio')
+    });
+    self.set('canvas', canvas);
+    self.updateCanvas();
+  }
+  initViewport() {
+    const cfgs = this._cfgs;
+    const size = cfgs.size;
+    const graph = cfgs.graph;
+    const pixelRatio = graph.get('pixelRatio') || graph.get('canvas').get('pixelRatio');
+    const widthRatio = graph.get('width') / size[0] * pixelRatio;
+    const heightRatio = graph.get('height') / size[1] * pixelRatio;
+    const canvas = this.get('canvas');
+    const containerDOM = canvas.get('containerDOM');
+    const viewport = createDOM('<div class="' + cfgs.viewportClassName + '" style="position:absolute;left:0;top:0;box-sizing:border-box;border: 2px solid #1980ff"></div>');
+    let x,            // 计算拖拽水平方向距离
+      y,              // 计算拖拽垂直方向距离
+      dragging,       // 是否在拖拽minimap的视口
+      left,           // 缓存viewport当前对于画布的x
+      top,            // 缓存viewport当前对于画布的y
+      width,          // 缓存viewport当前宽度
+      height;         // 缓存viewport当前高度
+    containerDOM.addEventListener('mousedown', e => {
+      if (e.target !== viewport) {
+        return;
+      }
+      // 如果视口已经最大了,不需要拖拽
+      const style = viewport.style;
+      left = parseInt(style.left, 10);
+      top = parseInt(style.top, 10);
+      width = parseInt(style.width, 10);
+      height = parseInt(style.height, 10);
+      if (width >= size[0] || height >= size[1]) {
+        return;
+      }
+      dragging = true;
+      x = e.clientX;
+      y = e.clientY;
+    }, false);
+    containerDOM.addEventListener('mousemove', e => {
+      if (!dragging || isNil(e.clientX) || isNil(e.clientY)) {
+        return;
+      }
+      let dx = x - e.clientX;
+      let dy = y - e.clientY;
+      // 若视口移动到最左边或最右边了,仅移动到边界
+      if (left - dx < 0) {
+        dx = left;
+      } else if (left - dx + width > size[0]) {
+        dx = left + width - size[0];
+      }
+      // 若视口移动到最上或最下边了,仅移动到边界
+      if (top - dy < 0) {
+        dy = top;
+      } else if (top - dy + height > size[1]) {
+        dy = top + height - size[1];
+      }
+      left -= dx;
+      top -= dy;
+      // 先移动视口,避免移动到边上以后出现视口闪烁
+      modifyCSS(viewport, {
+        left: left + 'px',
+        top: top + 'px'
+      });
+      graph.translate(dx * widthRatio, dy * heightRatio);
+      x = e.clientX;
+      y = e.clientY;
+    }, false);
+    containerDOM.addEventListener('mouseleave', () => {
+      dragging = false;
+    }, false);
+    containerDOM.addEventListener('mouseup', () => {
+      dragging = false;
+    }, false);
+    this.set('viewport', viewport);
+    containerDOM.appendChild(viewport);
+  }
+  updateCanvas() {
+    const size = this.get('size');
+    const graph = this.get('graph');
+    const canvas = this.get('canvas');
+    // 根据cfgs更新画布内容
+    if (this.get('keyShapeOnly')) {
+      this._updateKeyShapes();
+    } else {
+      this._updateGraphShapes();
+    }
+    // 更新minimap视口
+    this._updateViewport();
+    // 刷新后bbox可能会变,需要重置画布矩阵以缩放到合适的大小
+    const bbox = canvas.getBBox();
+    const width = max(bbox.width, graph.get('width'));
+    const height = max(bbox.height, graph.get('height'));
+    const pixelRatio = canvas.get('pixelRatio');
+    canvas.resetMatrix();
+    canvas.scale(size[0] / width * pixelRatio, size[1] / height * pixelRatio);
+    canvas.draw();
+  }
+  // 仅在minimap上绘制keyShape
+  // FIXME 如果用户自定义绘制了其他内容,minimap上就无法画出
+  _updateKeyShapes() {
+    const graph = this._cfgs.graph;
+    const canvas = this.get('canvas');
+    const group = canvas.get('children')[0] || canvas.addGroup();
+    const nodes = graph.getNodes();
+    const edges = graph.getEdges();
+    canvas.get('children');
+    // 边可以直接使用keyShape
+    each(edges, edge => {
+      group.add(edge.get('keyShape').clone());
+    });
+    // 节点需要group配合keyShape
+    each(nodes, node => {
+      const parent = group.addGroup();
+      parent.setMatrix(node.get('group').attr('matrix'));
+      parent.add(node.get('keyShape').clone());
+    });
+  }
+  // 将主图上的图形完全复制到小图
+  _updateGraphShapes() {
+    const cfgs = this._cfgs;
+    const graph = cfgs.graph;
+    const canvas = this.get('canvas');
+    const graphGroup = graph.get('group');
+    const clonedGroup = graphGroup.clone();
+    clonedGroup.resetMatrix();
+    canvas.get('children')[0] = clonedGroup;
+  }
+  // 绘制minimap视口
+  _updateViewport() {
+    const size = this._cfgs.size;
+    const graph = this._cfgs.graph;
+    const matrix = graph.get('group').getMatrix();
+    const topLeft = graph.getPointByCanvas(0, 0);
+    const viewport = this.get('viewport');
+    if (!viewport) {
+      this.initViewport();
+    }
+    // viewport宽高,左上角点的计算
+    const width = matrix[0] >= 1 ? size[0] / matrix[0] : size[0];
+    const height = matrix[4] >= 1 ? size[1] / matrix[4] : size[1];
+    const left = topLeft.x > 0 ? topLeft.x * size[0] / graph.get('width') : 0;
+    const top = topLeft.y > 0 ? topLeft.y * size[1] / graph.get('height') : 0;
+    modifyCSS(viewport, {
+      left: left + 'px',
+      top: top + 'px',
+      width: width + 'px',
+      height: height + 'px'
+    });
+  }
+
+  /**
+   * 获取minimap的画布
+   * @return {object} G的canvas实例
+   */
+  getCanvas() {
+    return this.get('canvas');
+  }
+  /**
+   * 获取minimap的窗口
+   * @return {object} 窗口的dom实例
+   */
+  getViewport() {
+    return this.get('viewport');
+  }
+  /**
+   * 获取minimap的容器dom
+   * @return {object} dom
+   */
+  getContainer() {
+    return this.get('container');
+  }
+  destroy() {
+    const container = this.get('canvas');
+    this.get('canvas').destroy();
+    container.innerHTML = '';
+    super.destroy();
+  }
+}
+
+module.exports = Minimap;

+ 16 - 0
src/graph/graph.js

@@ -49,6 +49,10 @@ class Graph extends EventEmitter {
        */
       mode: [],
       /**
+       * 注册插件
+       */
+      plugins: [],
+      /**
        * source data
        * @type object
        */
@@ -185,6 +189,7 @@ class Graph extends EventEmitter {
     const modeController = new Controller.Mode(this);
     const itemController = new Controller.Item(this);
     this.set({ eventController, viewController, modeController, itemController });
+    this._initPlugins();
   }
   _initCanvas() {
     let container = this.get('container');
@@ -216,6 +221,14 @@ class Graph extends EventEmitter {
     }
     this.set('group', group);
   }
+  _initPlugins() {
+    const self = this;
+    Util.each(self.get('plugins'), plugin => {
+      if (!plugin.destroyed && plugin.init) {
+        plugin.init(self);
+      }
+    });
+  }
   get(key) {
     return this._cfg[key];
   }
@@ -907,6 +920,9 @@ class Graph extends EventEmitter {
    */
   destroy() {
     this.clear();
+    Util.each(this.get('plugins'), plugin => {
+      plugin.destroy();
+    });
     this.get('eventController').destroy();
     this.get('itemController').destroy();
     this.get('modeController').destroy();

+ 19 - 1
webpack.config.js

@@ -1,11 +1,29 @@
 const webpack = require('webpack');
 const resolve = require('path').resolve;
+const shelljs = require('shelljs');
 const _ = require('lodash');
 
 const entry = {
-  G6: './src/index.js'
+  G6: './src/index.js',
+  G6Plugins: './plugins/index.js'
 };
 
+shelljs.ls(resolve(__dirname, 'plugins')).forEach(pluginPath => {
+  if (pluginPath === 'base.js') return;
+  if (pluginPath !== 'index.js') {
+    const fileDirs = pluginPath.split('-');
+    let moduleName = '';
+    for (let i = 0; i < fileDirs.length; i++) {
+      const segment = fileDirs[i];
+      moduleName += (segment.charAt(0).toUpperCase() + segment.substring(1));
+    }
+    entry[moduleName] = `./plugins/${pluginPath}/index.js`;
+  } else {
+    const moduleName = 'plugins';
+    entry[moduleName] = './plugins/index.js';
+  }
+});
+
 module.exports = {
   mode: 'production',
   devtool: 'cheap-source-map',