docs: function layoutGraph(graph) {
    // create a map for referencing node position by node id.
    const nodePositionMap = graph.nodes.reduce((res, node) => {
      res[node.id] = node.position;
      return res;
    }, {});
    // bucket edges between the same source/target node pairs.
    const nodePairs = graph.edges.reduce((res, edge) => {
      const nodes = [edge.sourceId, edge.targetId];
      // sort the node ids to count the edges with the same pair
      // but different direction (a -> b or b -> a)
      const pairId = nodes.sort().toString();
      // push this edge into the bucket
      if (!res[pairId]) {
        res[pairId] = [edge];
      } else {
        res[pairId].push(edge);
      }
      return res;
    }, {});
    // start to create curved edges
    const unitOffset = 30;
    const layoutEdges = Object.keys(nodePairs).reduce((res, pairId) => {
      const edges = nodePairs[pairId];
      const curved = edges.length > 1;
      // curve line is directional, pairId is a list of sorted node ids.
      const nodeIds = pairId.split(',');
      const curveSourceId = nodeIds[0];
      const curveTargetId = nodeIds[1];
      // generate new edges with layout information
      const newEdges = edges.map((e, idx) => {
        // curve direction (1 or -1)
        const direction = idx % 2 ? 1 : -1;
        // straight line if there's only one edge between this two nodes.
        const offset = curved ? (1 + Math.floor(idx / 2)) * unitOffset : 0;
        return {
          ...e,
          source: nodePositionMap[e.sourceId],
          target: nodePositionMap[e.targetId],
          controlPoint: computeControlPoint(
            nodePositionMap[curveSourceId],
            nodePositionMap[curveTargetId],
            direction,
            offset
          )
        };
      });
      return res.concat(newEdges);
    }, []);
    return {
      nodes: graph.nodes,
      edges: layoutEdges
    };
  }