xyflow / xyflow

React Flow | Svelte Flow - Powerful open source libraries for building node-based UIs with React (https://reactflow.dev) or Svelte (https://svelteflow.dev). Ready out-of-the-box and infinitely customizable.
https://xyflow.com
MIT License
22.07k stars 1.46k forks source link

Edge not aligned with handle when scale is applied #1663

Closed litewarp closed 9 months ago

litewarp commented 2 years ago

Hi there. I've got an issue similar to #1075 and #1136 where the calculated edge path does not align with handles, and my use case may provide some insight onto why it's happening.

I use spectacle to create presentations and recently wanted to start using graphs created with react-flow in the slide. However, there is something strange happening when the slide is scaled down. The image below is from the graph rendered on a slide.

image

This one is the graph rendered outside of the Spectrum <Slide> component, but otherwise identical. The edges are correct.

image

I can confirm that it has something to do with the scale transform based on the slide's height and width. If I change the size of the slide and reduce its dimensions (e.g., 1280x720 instead of 1920x1080) the misalignment is less severe but still present.

If you use the Spectacle print mode, the edges are more accurate, but still slightly off.

image

This may be too complex of a use case for a fix, especially if it is the result of a misconfiguration of Spectacle. But I figured that it at least gives you a reproducible example of the misalignment issue.

Any ideas on how prevent this from happening when you're working with a graph contained in a non-standard series of DOM elements?

Thanks in advance. Love the library!

xplato commented 2 years ago

+1

This also applies to the ReactFlow component being nested inside of a will-transform component like so:

import { motion } from 'framer-motion';

// ...

<motion.div whileHover={{ scale: 1.05 }}>
    <ReactFlow />
</motion.div>

(The edges are mis-aligned exactly like the picture above even before a transform is made or set).

litewarp commented 2 years ago

As an FYI, my workaround was just to recalculate the elements myself in a custom edge.

In order to get the correct edgePath, you could do something like the following:

import React from 'react'
import {
  EdgeProps,
  getSmoothStepPath,
  getMarkerEnd,
  useStoreState,
  Position,
  Node,
  XYPosition,
  getBezierPath
} from 'react-flow-renderer'

export type EdgeVariant = 'linear' | 'smoothstep' | 'bezier'

type CustomEdgeProps = EdgeProps<{
  label?: string
  variant: EdgeVariant
}>

type LinearEdgePathProps = {
  sourceX: number
  sourceY: number
  targetX: number
  targetY: number
}

type StepBezierEdgePathProps = LinearEdgePathProps & {
  sourcePosition: Position
  targetPosition: Position
}

type EdgePathCalculationProps = StepBezierEdgePathProps & {
  variant: EdgeVariant
}

// copied from 'rf src/container/EdgeRenderer/utils.ts'
const getHandlePosition = (
  position: Position,
  node: Node,
  handle: any | null = null
): XYPosition => {
  const x = (handle?.x || 0) + node.__rf.position.x
  const y = (handle?.y || 0) + node.__rf.position.y
  const width = handle?.width || node.__rf.width
  const height = handle?.height || node.__rf.height

  switch (position) {
    case Position.Top:
      return {
        x: x + width / 2,
        y
      }
    case Position.Right:
      return {
        x: x + width,
        y: y + height / 2
      }
    case Position.Bottom:
      return {
        x: x + width / 2,
        y: y + height
      }
    case Position.Left:
      return {
        x,
        y: y + height / 2
      }
  }
}

const getCustomEdgePath = ({
  variant,
  sourceX,
  sourceY,
  targetX,
  targetY,
  sourcePosition,
  targetPosition
}: EdgePathCalculationProps): string => {
  switch (variant) {
    case 'bezier':
      return getBezierPath({
        sourceX,
        sourceY,
        sourcePosition,
        targetX,
        targetY,
        targetPosition
      })
    case 'smoothstep':
      return getSmoothStepPath({
        sourceX,
        sourceY,
        sourcePosition,
        targetX,
        targetY,
        targetPosition
      })
    case 'linear':
      return `M ${sourceX},${sourceY}L ${targetX},${targetY}`
  }
}

const CustomEdge: React.FC<CustomEdgeProps> = ({
  id,
  style = {},
  sourcePosition,
  targetPosition,
  data,
  arrowHeadType,
  markerEndId,
  source,
  target
}) => {
  const nodes = useStoreState((state) => state.nodes)

  const sourceNode = nodes.find((node) => node.id === source)
  const sourceHandle = getHandlePosition(sourcePosition, sourceNode as Node, null)

  const targetNode = nodes.find((node) => node.id === target)
  const targetHandle = getHandlePosition(targetPosition, targetNode as Node, null)

  const markerEnd = getMarkerEnd(arrowHeadType, markerEndId)

  const edgePath = getCustomEdgePath({
    sourceX: sourceHandle.x,
    sourceY: sourceHandle.y,
    sourcePosition,
    targetX: targetHandle.x,
    targetY: targetHandle.y,
    targetPosition,
    variant: data?.variant ?? 'bezier'
  })

  return (
    <>
      <path
        id={id}
        style={style}
        className="react-flow__edge-path"
        d={edgePath}
        markerEnd={markerEnd}
      />
      {data?.label ? (
        <text>
          <textPath
            href={`#${id}`}
            style={{ fontSize: '12px' }}
            startOffset="50%"
            textAnchor="middle"
          >
            {data.label}
          </textPath>
        </text>
      ) : null}
    </>
  )
}
moklick commented 2 years ago

Could you find a solution? If not, could you send a minimal example within a codesandbox?

litewarp commented 2 years ago

The solution was just to recalculate the edges per the component above ☝🏾

I'm not sure what exactly happens when the EdgeRenderer calculates the nodes initially but if I do the calculation myself it mostly turns out ok. So I think there's some translation or transform happening.

I can try and put together a repro tomorrow.

moklick commented 2 years ago

Ok. Great that you could fix it! No need to rush with the repo but it seems to be an interesting issue.

litewarp commented 2 years ago

Codesandbox here

If you just render the <TestFlow /> element, the edges are aligned. If you render it within the Spectacle Components, the edges are misaligned.

image

RohitMishraPU commented 2 years ago

Even I am facing the same issue. Any update or help would be great .

Sec-ant commented 2 years ago

I encountered a similar problem recently and found out it was the animation hadn't stopped when the node internal position was calculated. So I added a onAnimationEnd handler and used the useUpdateNodeInternals hook to recalculate node internals once the animation ended and the problem was solved.

moklick commented 2 years ago

I am sure the issue here is coming from some transform styles from Spectacle but I have no solution for this..

pdambrauskas commented 1 year ago

We faced the same problem, when we used React Flow component inside a modal, which had animation css property. Removing animation property fixed the problem.

KhudhurMD commented 1 year ago

Thanks @pdambrauskas

For me, I just stopped using the "scale" from CSS attribte "transform". It then worked perfectly!

coopbri commented 9 months ago

I encountered a similar problem recently and found out it was the animation hadn't stopped when the node internal position was calculated. So I added a onAnimationEnd handler and used the useUpdateNodeInternals hook to recalculate node internals once the animation ended and the problem was solved.

Thank you! This worked for me inside of a Spectacle slide as well:

import { FlexBox, Slide } from "spectacle";
import { useUpdateNodeInternals } from "reactflow";

// ...

<Slide>
  <FlexBox onAnimationEnd={useUpdateNodeInternals}>
    {/* ... */}
  </FlexBox>
</Slide>
moklick commented 9 months ago

If you are doing an animation that changes the scale, you need to update the node internals via ]useUpdateNodeInternals|(https://reactflow.dev/docs/api/hooks/use-update-node-internals/) to let React Flow re-measure the handle positions.

aryankarim commented 2 months ago

Having the same issue with vue-flow

4096void commented 3 weeks ago

I encountered a similar problem recently and found out it was the animation hadn't stopped when the node internal position was calculated. So I added a onAnimationEnd handler and used the useUpdateNodeInternals hook to recalculate node internals once the animation ended and the problem was solved.

Thank you! This worked for me inside of a Spectacle slide as well:

import { FlexBox, Slide } from "spectacle";
import { useUpdateNodeInternals } from "reactflow";

// ...

<Slide>
  <FlexBox onAnimationEnd={useUpdateNodeInternals}>
    {/* ... */}
  </FlexBox>
</Slide>

I encountered kind of more or less similar situation but within MUI's Zoom component, and solved by delay mounting of reactflow, and the delayed wrapper

<Delayed>
  {your_reactflow_component}
</Delayed>