Source: @@graph/Graph.js

/**
 * @file Handling of interactions and side-effects in the AntV graph instance
 * @module @@graph/Graph
 */
import { curry, fromPairs, path, view, groupBy, head, prop } from 'ramda';
import {Canvas as CanvasWrapper} from '@@graph/wrappers';
import {Property, Node, Edge} from '@@graph/handlers';
import { Handler } from '@@graph/handlers/Handler';
import { getNodes, NODE_TYPE } from '@@graph/Node';
import { getEdges, Edge as GraphEdge } from '@@graph/Edge';
import { dispatch } from '@@app-state';
import { registerNewClassWithCallback, properties } from '@@app-state/model/state';
import * as ModelState from '@@app-state/model/state';

const getWrapper = n => {
  if (!n) return;
  return n.getParent && n.getParent() && n.getParent().get('wrapper');  // Group wrapper
}

const handle = curry((methodName, e) => {
  e.stopPropagation();
  const wrapper = getWrapper(e.target) || (e.item && e.item.get('wrapper')); // Group wrapper for nodes or Edge wrapper

  // Default to CanvasWrapper if there's no handler which could resolve the function call
  if (wrapper && typeof wrapper[methodName] == 'function') {
    return wrapper[methodName](e.target || e.item);
  } else {
    return CanvasWrapper[methodName] && CanvasWrapper[methodName](e.target, e);
  }
});

export class Graph {
  static behaviours = {
    click: 'onClick',
    touchend: 'onClick',
    mouseover: 'onHover',
    mouseout: 'onBlur'
  };

  static setInstance(graph) {
    this.instance = graph;
  }

  static transformModelToGraphData(state) {
    const props = view(properties, state);

    return Object.values(props)
      .reduce((acc, {dataProperty, source, target, predicate}) => {
        acc[source] = acc[source] || {
          dataProperties: {},
          objectProperties: {}
        };

        if (dataProperty) {
          acc[source].dataProperties[predicate] = acc[source].dataProperties[predicate] || [];
          acc[source].dataProperties[predicate].push(target);
        } else {
          acc[source].objectProperties[predicate] = acc[source].objectProperties[predicate] || [];
          acc[source].objectProperties[predicate].push(target);
        }

        return acc;
      }, {});
  }

  static loadDataFromState(state) {
    const data = this.transformModelToGraphData(state);
    this.loadData(data);
  }

  static loadData(data) {
    console.time('getNodes')
    const nodes = getNodes(data);
    console.timeEnd('getNodes')
    console.time('getEdges')
    const edges = getEdges(data);
    console.timeEnd('getEdges');
    this.instance.data({nodes, edges});
  }

  static getBBoxesById() {
    return fromPairs(this.instance
      .getNodes()
      .filter(n => !n.getContainer().destroyed)
      .map(n => [n.get('id'), {bbox: n.getBBox()}]));
  }

  static updatePositions(dataById) {
    const moveFns = [];
    this.instance.getNodes().forEach(n => {
      const bbox = path([n.get('id'), 'bbox'], dataById);
      if (bbox) {
        const {x, y} = bbox;
        moveFns.push(() => {
          n.get('group').get('item').updatePosition({x, y});
          n.get('group').get('wrapper').onLoad();
          n.getEdges().forEach(e => e.refresh());
        });
      }
    })

    // Trigger the move functions only after the graph is done layouting (to prevent delayed zoom changes
    // messing up the placement)
    this.instance.on('afterlayout', () => {
      moveFns.forEach(f => f());
      this.instance.fitView()
      this.instance.off('afterlayout');
    });
  }

  static initialize(data) {
    data = data || this.data;
    console.time('this.@@graph.clear();')
    this.instance.clear();
    console.timeEnd('this.@@graph.clear();')

    console.time('this.deregisterBehaviours();')
    this.deregisterBehaviours();
    console.timeEnd('this.deregisterBehaviours();')

    console.time('this.registerBehaviours();')
    this.registerBehaviours();
    console.timeEnd('this.registerBehaviours();')

    console.time('this.graph.data();')
    if (view(ModelState.rootLens, data)) {
      this.loadDataFromState(data);
    } else {
      this.loadData(data);
    }
    console.timeEnd('this.graph.data();')

    console.time('this.render();')
    this.render();
    console.timeEnd('this.render();')
    this.data = data;
  }

  static registerBehaviours() {
    Object.entries(this.behaviours).forEach(([key, targetMethod]) => this.instance.on(key, handle(targetMethod)));
  }

  static deregisterBehaviours() {
    Object.keys(this.behaviours).forEach(key => this.instance.off(key));
  }

  static clear() {
    Handler.clear();
    Property.clear();
    Node.clear();
    Edge.clear();
    if (this.instance) {
      this.instance.clear();
    }
  }

  static render() {
    this.instance.render();
    Property.commitResources();
    Node.commitResources();
    Edge.commitResources();
    Handler.bindProperties();
  }

  static destroy() {
    this.instance.destroy();
  }

  static onCreateNewEntity(id) {
    this.copyNode(Handler.recipients[id].getGroupController().group);
  }

  static onDeleteEntity(id) {
    Handler.remove(id);
    this.instance.removeItem(id);
    dispatch(ModelState.deleteClass(id));
  }

  static removeItem(item) {
    this.instance.removeItem(item);
  }

  /**
   * Copies given node --> creates a new class in the model and adds the node to the graph
   * @function
   * @param node
   */
  static copyNode(node) {
    const {cfg} = node;

    const {x, y, width, height} = node.get('item').getBBox();
    dispatch(registerNewClassWithCallback(cfg.id, ({newId: id, instance, properties}) => {
      const node = this.instance.addItem('node', {
        id,
        varName: instance.varName,
        label: instance.type,
        data: cfg.data,
        type: NODE_TYPE,
        x: x + width / 2,
        y: y - 2*height
      });

      node.toFront();

      const groupedProperties =
        groupBy(
          ([, p]) => [p.target, p.source].sort().join('-'),
          Object.entries(properties).filter(([, p]) => !p.dataProperty)
        );


      // Create edges for target/source pair
      Object.values(groupedProperties)
        .forEach(propertiesForGivenPair => {
          const propertyIds = propertiesForGivenPair.map(head);
          const properties = propertiesForGivenPair.flatMap(prop(1));
          const {source, target} = properties[0];

          properties.forEach(p => {
            if (p.target === id) {
              Handler.recipients[p.source].getGroupController().group.get('addProperty')(p);
            }
          });

          if (source !== target) {  // There should be no loop edges
            const edge = this.instance.addItem('edge', GraphEdge({
              source, target,
              propertyIds,
              data: {
                source, target
              }
            }));

            if (
              path(['recipients', target, 'state', 'hidden'], Handler)
              || path(['recipients', source, 'state', 'hidden'], Handler)
            ) {
              edge.hide();
            }

            Handler.recipients[source].getGroupController().recalculateEdges();
            Handler.recipients[target].getGroupController().recalculateEdges();
          }
        })
    }))
  }
}