function Sankey()

in packages-ext/recoil-devtools/src/utils/sankey/Sankey.js [96:430]


function Sankey<N, L>({
  height,
  width,
  margin = 10,
  orientation,
  layout: layoutFunction,
  links: linkData,
  nodes: nodeData,
  getNodeTooltip,
  nodeStyles,
  nodeClass,
  nodeLabelStyles,
  nodeLabelClass,
  nodeEvents,
  nodeThickness = 20,
  nodeLabelAlignment = 'inward',
  getLinkTooltip,
  linkColor,
  linkStyles,
  linkClass,
  linkEvents,
  linkCurvature = 0.5,
  linkThickPathThreshold = 0.75,
  valueFormatter = String,
  animationDurationMS = 2000,
  defs,
}: Props<N, L>): React.MixedElement {
  const ref = useRef<?Element>(null);

  // Chart Size
  const positionRange = useMemo(
    () => [0, (orientation === 'horizontal' ? height : width) - margin * 2],
    [orientation, height, width, margin],
  );
  const depthRange = useMemo(
    () => [
      0,
      (orientation === 'horizontal' ? width : height) -
        nodeThickness -
        margin * 2,
    ],
    [orientation, height, width, margin, nodeThickness],
  );

  // Generate Graph
  const graph = useMemo(
    () => generateGraph<N, L>({nodeData, linkData}),
    [nodeData, linkData],
  );

  // Layout Graph
  const layout = useMemo(
    () =>
      layoutFunction({
        graph,
        positionRange,
        depthRange,
      }),
    [depthRange, graph, layoutFunction, positionRange],
  );

  // Render Sankey via D3
  useLayoutEffect(() => {
    // Setup Scales
    const depth = d3Scale
      .scaleLinear()
      .domain(layout.depthDomain)
      .rangeRound(depthRange);
    const breadth = d3Scale
      .scaleLinear()
      .domain(layout.positionDomain)
      .range(positionRange);

    // Select host <svg>
    if (ref.current == null) {
      return;
    }
    const svg = select(ref.current, {animationDurationMS});

    // Bind Data
    const linksSelection = svg.select('g.links').bind(
      'g.link',
      layout.graph.links.filter(l => l.visible),
      l => l.key,
    );
    const nodesSelection = svg.select('g.nodes').bind(
      'svg.node',
      layout.graph.nodes.filter(n => n.visible),
      n => n.key,
    );

    // Render Nodes
    const nodeDepth = n => depth(n.depth);
    const nodePosition = n => breadth(n.position);
    const nodeWidth = n => breadth(n.value);
    nodesSelection.attr({
      x: orientation === 'horizontal' ? nodeDepth : nodePosition,
      y: orientation === 'horizontal' ? nodePosition : nodeDepth,
      width: orientation === 'horizontal' ? nodeThickness : nodeWidth,
      height: orientation === 'horizontal' ? nodeWidth : nodeThickness,
    });
    // Node Rects
    nodesSelection
      .select('rect')
      .attr({width: '100%', height: '100%', class: nodeClass ?? null})
      .style(nodeStyles)
      .on(nodeEvents);

    // Node Tooltips
    nodesSelection
      .select('title')
      .text(
        getNodeTooltip
          ? n => getNodeTooltip(n)
          : n => `${n.name}\n${valueFormatter(n.value)}`,
      );

    // Render Labels
    const labelsSelection = nodesSelection.select('text').text(n => n.name);
    const labelAlignRight = n =>
      nodeLabelAlignment === 'right' || n.depth <= layout.depthDomain[1] / 2;
    if (orientation === 'horizontal') {
      nodesSelection.style({overflow: 'visible'});
      labelsSelection.attr({
        y: n => nodeWidth(n) / 2,
        x: n => (labelAlignRight(n) ? nodeThickness : 0),
        dx: n => (labelAlignRight(n) ? '0.25em' : '-0.25em'),
        'text-anchor': n => (labelAlignRight(n) ? 'start' : 'end'),
        'dominant-baseline': 'middle',
      });
    } else {
      nodesSelection.style({overflow: 'hidden'});
      labelsSelection.attr({
        x: n => breadth(n.value) / 2,
        dy: nodeThickness / 2,
        'text-anchor': 'middle',
        'dominant-baseline': 'middle',
      });
    }
    labelsSelection
      .attr({class: nodeLabelClass ?? ''})
      .style({'pointer-events': 'none'})
      .style(nodeLabelStyles);

    // Link Tooltips
    linksSelection
      .select('title')
      .html(l =>
        getLinkTooltip
          ? getLinkTooltip(l)
          : `${l.source?.name ?? '[UNKNOWN]'} &nbsp;&#8594;&nbsp; ${
              l.target?.name ?? '[UNKNOWN]'
            }\n${valueFormatter(l.value)}`,
      );

    const isThickCurve = (l: Link<L, N>) => {
      const depthDelta = depth(l.targetDepth) - depth(l.sourceDepth);
      return (
        depthDelta > nodeThickness &&
        breadth(l.value) > depthDelta * linkThickPathThreshold
      );
    };

    // Render Links
    linksSelection
      .select('path')
      .attr({
        d: l => {
          const linkBreadth = breadth(l.value);
          const sourceDepth = depth(l.sourceDepth) + nodeThickness;
          const targetDepth = depth(l.targetDepth);

          // Control point depths to define link curvature
          // Allow back-edges to swoop around
          const isBackEdgeCurve = targetDepth <= sourceDepth;
          const depthInterpolator = d3Interpolate.interpolateRound(
            sourceDepth,
            targetDepth,
          );
          const backEdgeCurvature = isBackEdgeCurve
            ? Math.pow(l.sourceDepth - l.targetDepth, 0.5) + 1
            : 0;
          const sourceControlPointDepth = !isBackEdgeCurve
            ? depthInterpolator(linkCurvature)
            : sourceDepth + (depth(backEdgeCurvature) - depth(0));
          const targetControlPointDepth = !isBackEdgeCurve
            ? depthInterpolator(1 - linkCurvature)
            : targetDepth - (depth(backEdgeCurvature) - depth(0));

          // Browsers can introduce rendering artifacts if curves are too wide,
          // to avoid this, if a link is too thick then outline and fill the path instead.
          if (!isThickCurve(l)) {
            // Browsers may not apply the fade gradient mask properly for a straight
            // path.  In this case, jitter the faded end of the line slightly.
            // (As of Chrome 9/3/20)
            const isStraight =
              breadth(l.sourcePosition).toFixed(3) ===
              breadth(l.targetPosition).toFixed(3);
            const sourcePosition =
              breadth(l.sourcePosition + l.value / 2) +
              (l.fadeSource && isStraight ? 1 : 0);
            const targetPosition =
              breadth(l.targetPosition + l.value / 2) +
              (l.fadeTarget && isStraight ? 1 : 0);
            return orientation === 'horizontal'
              ? `M${sourceDepth},${sourcePosition}` + // Start of curve
                  `C${sourceControlPointDepth},${sourcePosition}` + // First control point
                  ` ${targetControlPointDepth},${targetPosition}` + // Second conrol point
                  ` ${targetDepth},${targetPosition}` // End of curve
              : `M${sourcePosition},${sourceDepth}` + // Start of curve
                  `C${sourcePosition},${sourceControlPointDepth}` + // First control point
                  ` ${targetPosition},${targetControlPointDepth}` + // Second conrol point
                  ` ${targetPosition},${targetDepth}`; // End of curve
          } else {
            const sourcePosition = breadth(l.sourcePosition);
            const targetPosition = breadth(l.targetPosition);
            return orientation === 'horizontal'
              ? `M${sourceDepth},${sourcePosition}` + // Start of curve
                  `C${sourceControlPointDepth},${sourcePosition}` + // First control point
                  ` ${targetControlPointDepth},${targetPosition}` + // Second conrol point
                  ` ${targetDepth},${targetPosition}` + // End of curve
                  `v${linkBreadth}` +
                  `C${targetControlPointDepth},${
                    targetPosition + linkBreadth
                  }` + // Second control point
                  ` ${sourceControlPointDepth},${
                    sourcePosition + linkBreadth
                  }` + // First conrol point
                  ` ${sourceDepth},${sourcePosition + linkBreadth}` +
                  `Z`
              : `M${sourcePosition},${sourceDepth}` + // Start of curve
                  `C${sourcePosition},${sourceControlPointDepth}` + // First control point
                  ` ${targetPosition},${targetControlPointDepth}` + // Second conrol point
                  ` ${targetPosition},${targetDepth}` + // End of curve
                  `h${linkBreadth}` +
                  `C${
                    targetPosition + linkBreadth
                  },${targetControlPointDepth}` + // Second control point
                  ` ${
                    sourcePosition + linkBreadth
                  },${sourceControlPointDepth}` + // First conrol point
                  ` ${sourcePosition + linkBreadth},${sourceDepth}` +
                  `Z`;
          }
        },
        'stroke-width': l =>
          !isThickCurve(l) ? Math.max(1, breadth(l.value)) : 0,
        mask: l =>
          l.fadeSource
            ? 'url(#mask_fade_left)'
            : l.fadeTarget
            ? 'url(#mask_fade_right)'
            : null,
        class: linkClass ?? '',
      })
      .style({
        ...linkStyles,
        fill: l => (isThickCurve(l) ? functor(linkColor)(l) : 'none'),
        stroke: l => (isThickCurve(l) ? 'none' : functor(linkColor)(l)),
        'fill-opacity': l => (isThickCurve(l) ? 1 : 0),
        'stroke-opacity': l => (isThickCurve(l) ? 0 : 1),
      })
      .on(linkEvents);
  }, [
    animationDurationMS,
    layout,
    depthRange,
    getLinkTooltip,
    getNodeTooltip,
    linkClass,
    linkColor,
    linkCurvature,
    linkEvents,
    linkStyles,
    linkThickPathThreshold,
    nodeClass,
    nodeEvents,
    nodeLabelAlignment,
    nodeLabelClass,
    nodeLabelStyles,
    nodeStyles,
    nodeThickness,
    orientation,
    positionRange,
    valueFormatter,
  ]);

  return (
    <svg
      ref={ref}
      height={height}
      width={width}
      viewBox={`${-margin} ${-margin} ${width} ${height}`}>
      <defs>
        <linearGradient id="gradient_for_mask_fade_right">
          <stop offset="0.5" stopColor="white" stopOpacity="1" />
          <stop offset="0.9" stopColor="white" stopOpacity="0" />
        </linearGradient>
        <linearGradient id="gradient_for_mask_fade_left">
          <stop offset="0.1" stopColor="white" stopOpacity="0" />
          <stop offset="0.5" stopColor="white" stopOpacity="1" />
        </linearGradient>
        <mask
          id="mask_fade_right"
          maskContentUnits="objectBoundingBox"
          x="-1"
          y="-500000%"
          height="1000000%"
          width="2">
          <rect
            x="-1"
            y="-500000"
            height="1000000"
            width="2"
            fill="url(#gradient_for_mask_fade_right)"
          />
        </mask>
        <mask
          id="mask_fade_left"
          maskContentUnits="objectBoundingBox"
          y="-500000%"
          height="1000000%"
          width="2">
          <rect
            y="-500000"
            height="1000000"
            width="2"
            fill="url(#gradient_for_mask_fade_left)"
          />
        </mask>
        {defs}
      </defs>
    </svg>
  );
}