_cluster(childNodesObj, childEdgesObj, options, refreshData = true) {
    // Remove nodes which are already clustered
    const tmpNodesToRemove = [];
    for (const nodeId in childNodesObj) {
      if (Object.prototype.hasOwnProperty.call(childNodesObj, nodeId)) {
        if (this.clusteredNodes[nodeId] !== undefined) {
          tmpNodesToRemove.push(nodeId);
        }
      }
    }

    for (let n = 0; n < tmpNodesToRemove.length; ++n) {
      delete childNodesObj[tmpNodesToRemove[n]];
    }

    // kill condition: no nodes don't bother
    if (Object.keys(childNodesObj).length == 0) {
      return;
    }

    // allow clusters of 1 if options allow
    if (
      Object.keys(childNodesObj).length == 1 &&
      options.clusterNodeProperties.allowSingleNodeCluster != true
    ) {
      return;
    }

    let clusterNodeProperties = deepExtend({}, options.clusterNodeProperties);

    // construct the clusterNodeProperties
    if (options.processProperties !== undefined) {
      // get the childNode options
      const childNodesOptions = [];
      for (const nodeId in childNodesObj) {
        if (Object.prototype.hasOwnProperty.call(childNodesObj, nodeId)) {
          const clonedOptions = NetworkUtil.cloneOptions(childNodesObj[nodeId]);
          childNodesOptions.push(clonedOptions);
        }
      }

      // get cluster properties based on childNodes
      const childEdgesOptions = [];
      for (const edgeId in childEdgesObj) {
        if (Object.prototype.hasOwnProperty.call(childEdgesObj, edgeId)) {
          // these cluster edges will be removed on creation of the cluster.
          if (edgeId.substr(0, 12) !== "clusterEdge:") {
            const clonedOptions = NetworkUtil.cloneOptions(
              childEdgesObj[edgeId],
              "edge"
            );
            childEdgesOptions.push(clonedOptions);
          }
        }
      }

      clusterNodeProperties = options.processProperties(
        clusterNodeProperties,
        childNodesOptions,
        childEdgesOptions
      );
      if (!clusterNodeProperties) {
        throw new Error(
          "The processProperties function does not return properties!"
        );
      }
    }

    // check if we have an unique id;
    if (clusterNodeProperties.id === undefined) {
      clusterNodeProperties.id = "cluster:" + randomUUID();
    }
    const clusterId = clusterNodeProperties.id;

    if (clusterNodeProperties.label === undefined) {
      clusterNodeProperties.label = "cluster";
    }

    // give the clusterNode a position if it does not have one.
    let pos = undefined;
    if (clusterNodeProperties.x === undefined) {
      pos = this._getClusterPosition(childNodesObj);
      clusterNodeProperties.x = pos.x;
    }
    if (clusterNodeProperties.y === undefined) {
      if (pos === undefined) {
        pos = this._getClusterPosition(childNodesObj);
      }
      clusterNodeProperties.y = pos.y;
    }

    // force the ID to remain the same
    clusterNodeProperties.id = clusterId;

    // create the cluster Node
    // Note that allowSingleNodeCluster, if present, is stored in the options as well
    const clusterNode = this.body.functions.createNode(
      clusterNodeProperties,
      Cluster
    );
    clusterNode.containedNodes = childNodesObj;
    clusterNode.containedEdges = childEdgesObj;
    // cache a copy from the cluster edge properties if we have to reconnect others later on
    clusterNode.clusterEdgeProperties = options.clusterEdgeProperties;

    // finally put the cluster node into global
    this.body.nodes[clusterNodeProperties.id] = clusterNode;

    this._clusterEdges(
      childNodesObj,
      childEdgesObj,
      clusterNodeProperties,
      options.clusterEdgeProperties
    );

    // set ID to undefined so no duplicates arise
    clusterNodeProperties.id = undefined;

    // wrap up
    if (refreshData === true) {
      this.body.emitter.emit("_dataChanged");
    }
  }
@param {object}    childNodesObj         | object with node objects, id as keys, same as childNodes except it also contains a source node
@param {object}    childEdgesObj         | object with edge objects, id as keys
@param {Array}     options               | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
@param {boolean}   refreshData | when true, do not wrap up
@private