openCluster(clusterNodeId, options, refreshData = true) {
    // kill conditions
    if (clusterNodeId === undefined) {
      throw new Error("No clusterNodeId supplied to openCluster.");
    }

    const clusterNode = this.body.nodes[clusterNodeId];

    if (clusterNode === undefined) {
      throw new Error(
        "The clusterNodeId supplied to openCluster does not exist."
      );
    }
    if (
      clusterNode.isCluster !== true ||
      clusterNode.containedNodes === undefined ||
      clusterNode.containedEdges === undefined
    ) {
      throw new Error("The node:" + clusterNodeId + " is not a valid cluster.");
    }

    // Check if current cluster is clustered itself
    const stack = this.findNode(clusterNodeId);
    const parentIndex = stack.indexOf(clusterNodeId) - 1;
    if (parentIndex >= 0) {
      // Current cluster is clustered; transfer contained nodes and edges to parent
      const parentClusterNodeId = stack[parentIndex];
      const parentClusterNode = this.body.nodes[parentClusterNodeId];

      // clustering.clusteredNodes and clustering.clusteredEdges remain unchanged
      parentClusterNode._openChildCluster(clusterNodeId);

      // All components of child cluster node have been transferred. It can die now.
      delete this.body.nodes[clusterNodeId];
      if (refreshData === true) {
        this.body.emitter.emit("_dataChanged");
      }

      return;
    }

    // main body
    const containedNodes = clusterNode.containedNodes;
    const containedEdges = clusterNode.containedEdges;

    // allow the user to position the nodes after release.
    if (
      options !== undefined &&
      options.releaseFunction !== undefined &&
      typeof options.releaseFunction === "function"
    ) {
      const positions = {};
      const clusterPosition = { x: clusterNode.x, y: clusterNode.y };
      for (const nodeId in containedNodes) {
        if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
          const containedNode = this.body.nodes[nodeId];
          positions[nodeId] = { x: containedNode.x, y: containedNode.y };
        }
      }
      const newPositions = options.releaseFunction(clusterPosition, positions);

      for (const nodeId in containedNodes) {
        if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
          const containedNode = this.body.nodes[nodeId];
          if (newPositions[nodeId] !== undefined) {
            containedNode.x =
              newPositions[nodeId].x === undefined
                ? clusterNode.x
                : newPositions[nodeId].x;
            containedNode.y =
              newPositions[nodeId].y === undefined
                ? clusterNode.y
                : newPositions[nodeId].y;
          }
        }
      }
    } else {
      // copy the position from the cluster
      forEach(containedNodes, function (containedNode) {
        // inherit position
        if (containedNode.options.fixed.x === false) {
          containedNode.x = clusterNode.x;
        }
        if (containedNode.options.fixed.y === false) {
          containedNode.y = clusterNode.y;
        }
      });
    }

    // release nodes
    for (const nodeId in containedNodes) {
      if (Object.prototype.hasOwnProperty.call(containedNodes, nodeId)) {
        const containedNode = this.body.nodes[nodeId];

        // inherit speed
        containedNode.vx = clusterNode.vx;
        containedNode.vy = clusterNode.vy;

        containedNode.setOptions({ physics: true });

        delete this.clusteredNodes[nodeId];
      }
    }

    // copy the clusterNode edges because we cannot iterate over an object that we add or remove from.
    const edgesToBeDeleted = [];
    for (let i = 0; i < clusterNode.edges.length; i++) {
      edgesToBeDeleted.push(clusterNode.edges[i]);
    }

    // actually handling the deleting.
    for (let i = 0; i < edgesToBeDeleted.length; i++) {
      const edge = edgesToBeDeleted[i];
      const otherNodeId = this._getConnectedId(edge, clusterNodeId);
      const otherNode = this.clusteredNodes[otherNodeId];

      for (let j = 0; j < edge.clusteringEdgeReplacingIds.length; j++) {
        const transferId = edge.clusteringEdgeReplacingIds[j];
        const transferEdge = this.body.edges[transferId];
        if (transferEdge === undefined) continue;

        // if the other node is in another cluster, we transfer ownership of this edge to the other cluster
        if (otherNode !== undefined) {
          // transfer ownership:
          const otherCluster = this.body.nodes[otherNode.clusterId];
          otherCluster.containedEdges[transferEdge.id] = transferEdge;

          // delete local reference
          delete containedEdges[transferEdge.id];

          // get to and from
          let fromId = transferEdge.fromId;
          let toId = transferEdge.toId;
          if (transferEdge.toId == otherNodeId) {
            toId = otherNode.clusterId;
          } else {
            fromId = otherNode.clusterId;
          }

          // create new cluster edge from the otherCluster
          this._createClusteredEdge(
            fromId,
            toId,
            transferEdge,
            otherCluster.clusterEdgeProperties,
            { hidden: false, physics: true }
          );
        } else {
          this._restoreEdge(transferEdge);
        }
      }

      edge.remove();
    }

    // handle the releasing of the edges
    for (const edgeId in containedEdges) {
      if (Object.prototype.hasOwnProperty.call(containedEdges, edgeId)) {
        this._restoreEdge(containedEdges[edgeId]);
      }
    }

    // remove clusterNode
    delete this.body.nodes[clusterNodeId];

    if (refreshData === true) {
      this.body.emitter.emit("_dataChanged");
    }
  }
Open a cluster by calling this function.
@param {vis.Edge.id}  clusterNodeId | the ID of the cluster node
@param {object} options
@param {boolean} refreshData | wrap up afterwards if not true