How Edge Curves and Bézier Paths Are Generated for Draw.io Diagrams

The graphviz2drawio library converts Graphviz SVG paths into Draw.io-compatible Bézier curves by parsing cubic and quadratic segments, subdividing non-linear curves at inflection points, and encoding the results as mxGraph style strings.

The hbmartin/graphviz2drawio repository bridges Graphviz DOT diagrams and Draw.io's editable format. When generating edge curves and bezier paths for Draw.io diagrams, the library transforms Graphviz's SVG vector output into the quadratic Bézier approximation that Draw.io's mxGraph engine expects.

Extracting SVG Path Data from Graphviz Output

The conversion pipeline begins in mx/EdgeFactory.py where the from_svg method processes the SVG group element representing a Graphviz edge. The factory locates the <path> element and extracts its d attribute, which contains the raw SVG path description.

if (path := SVG.get_first(g, "path")) is not None:
    if "d" in path.attrib:
        curve = self.curve_factory.from_svg(path.attrib["d"])

If the path data exists, the factory delegates parsing to CurveFactory.from_svg in mx/CurveFactory.py. This method uses the svg.path library to tokenize the SVG path string into a sequence of segment objects that represent the edge geometry.

Parsing Bézier Segments with CurveFactory

Once parsed, CurveFactory.from_svg iterates through each segment to build a Curve object. The factory distinguishes between quadratic and cubic Bézier curves, applying specialized transformation logic to ensure Draw.io compatibility.

Handling Quadratic Bézier Curves

For QuadraticBezier segments encountered in the SVG path, the factory directly extracts the control point and converts it to diagram coordinates using the coordinate translation system.

if isinstance(segment, QuadraticBezier):
    points.append(self.coords.complex_translate(segment.control))

These control points are stored in the Curve object's points list for direct use in the final Draw.io output.

Converting Cubic Bézier Curves

Cubic Bézier segments require additional processing to match Draw.io's quadratic curve expectations. The factory first checks if the segment is effectively linear using Curve.is_linear from mx/Curve.py. This function compares both control points against the line defined by the segment's start and end points using the tolerance defined in LINE_TOLERANCE. If linear, the start point is appended as a simple coordinate.

For non-linear cubic Béziers, the factory invokes inflection subdivision to break complex curves into manageable quadratic approximations:

elif isinstance(segment, CubicBezier):
    if Curve.is_linear(segment):
        points.append(self.coords.complex_translate(segment.start))
    else:
        split_cubes = subdivide_inflections(
            segment.start, segment.control1, segment.control2, segment.end,
        )
        split_controls = [
            self.coords.complex_translate(
                approximate_cubic_bezier_as_quadratic(*cube)[1],
            )
            for cube in split_cubes
            if cube
        ]
        points.extend(split_controls)

The subdivide_inflections function in mx/bezier.py calculates inflection points by finding the t parameters where the cubic's derivative cross-product indicates a change in concavity. It splits the curve at those points, then approximate_cubic_bezier_as_quadratic converts each resulting sub-curve into a quadratic Bézier.

The resulting Curve object stores the start point, end point, a boolean is_bezier flag (set to True when Bézier handling was required), and the list of quadratic control points.

Generating Draw.io Style Strings

The final transformation occurs in mx/Edge.py within the get_edge_style method. This method constructs the mxGraph style string that Draw.io interprets to render the edge properly.

if self.curve is not None:
    style = Styles.EDGE.format(... ) + (MxConst.CURVED if self.curve.is_bezier else MxConst.SHARP)

When is_bezier is true, the method appends MxConst.CURVED (encoding curved=1) to the style string, producing smooth edges. If false, it uses MxConst.SHARP for straight-line segments.

If source and target node geometries are available, the method calculates precise connection points along the node perimeters:

exit_x, exit_y = source_geo.relative_location_along_perimeter(self.curve.start)
entry_x, entry_y = target_geo.relative_location_along_perimeter(self.curve.end)
style += f"exitX={exit_x:.4f};exitY={exit_y:.4f};"
style += f"entryX={entry_x:.4f};entryY={entry_y:.4f};"

The complete style string (e.g., "edgeStyle=elbowEdgeStyle;curved=1;strokeColor=#000000;exitX=0.5;exitY=1.0;...") is embedded in the generated .drawio XML, instructing Draw.io to render the edge curves and bezier paths with proper geometric alignment.

Summary

  • mx/EdgeFactory.py extracts the SVG d attribute from Graphviz edge output and initiates curve parsing via CurveFactory.
  • mx/CurveFactory.py converts SVG path segments into Curve objects, handling quadratic curves directly while subdividing cubic Béziers at inflection points via subdivide_inflections.
  • mx/bezier.py provides the mathematical utilities for inflection detection (subdivide_inflections) and cubic-to-quadratic approximation (approximate_cubic_bezier_as_quadratic).
  • mx/Curve.py defines the Curve data class and implements linearity detection through Curve.is_linear.
  • mx/Edge.py generates the final mxGraph style string via get_edge_style, setting curved=1 for Bézier edges and calculating precise exitX/exitY and entryX/entryY coordinates relative to node geometries.

Frequently Asked Questions

How does graphviz2drawio handle complex cubic Bézier curves from Graphviz?

The library subdivides complex cubic Bézier curves at their inflection points using the subdivide_inflections function in mx/bezier.py. It finds the t parameters where the curve changes concavity by analyzing the derivative cross-product, splits the curve at those points, then approximates each resulting sub-curve as a quadratic Bézier using approximate_cubic_bezier_as_quadratic. This produces smooth curves that Draw.io can render while maintaining mathematical fidelity to the original Graphviz output.

What is the difference between curved and sharp edges in the generated Draw.io output?

According to mx/Edge.py, the get_edge_style method checks the Curve.is_bezier boolean flag. If true, it appends MxConst.CURVED (translating to curved=1 in the style string) to produce smooth Bézier edges. If false, it uses MxConst.SHARP to create straight-line segments. This determination happens during the SVG parsing phase when CurveFactory detects whether the path contains actual curve segments or linear connections.

Why does the library convert cubic Bézier curves to quadratic approximations?

Draw.io's mxGraph engine primarily supports quadratic Bézier curves for its curved edge rendering, while Graphviz outputs cubic Bézier curves in its SVG paths. The mx/CurveFactory.py implementation converts these cubic segments to quadratic approximations through the approximate_cubic_bezier_as_quadratic function, ensuring compatibility with Draw.io's rendering capabilities while preserving the visual trajectory of the original diagram.

Where does the library calculate connection points for edges on node boundaries?

The get_edge_style method in mx/Edge.py calculates exit and entry coordinates using source_geo.relative_location_along_perimeter(self.curve.start) and target_geo.relative_location_along_perimeter(self.curve.end). These calls determine the proportional positions along the source and target node perimeters where the edge should attach, then encode them as exitX, exitY, entryX, and entryY parameters in the mxGraph style string.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →