Skip to content

feat(plugins): fix edge parents in every generated .drawio - #64

Closed
taku0 wants to merge 1 commit into
jgraph:mainfrom
taku0:feat/fix-edge-parents
Closed

taku0 wants to merge 1 commit into
jgraph:mainfrom
taku0:feat/fix-edge-parents

Conversation

@taku0

@taku0 taku0 commented Aug 10, 2026

Copy link
Copy Markdown

Summary

ELK auto-layout (--layout) misplaces the edges of the generated diagrams because the drawio-mcp plugin sets the parent attributes to "1", the default layer, but ELK auto-layout expects the attributes to be the nearest common ancestors of their source and target cells.

This PR adds a Python script to fix it. The script is a port of mxGraphModel.updateEdgeParent from draw.io.

Sample diagrams

Those diagrams are generated from the same prompt with the plugin from the main branch and the branch of this PR. The diagram is converted to PNG with auto-layout by drawio -x -f png -e -b 10 --layout verticalFlow. The styles varies since the prompt doesn't specify it.

The main branch:
test_01_layed_out

File before auto-layout: test_01.drawio

The branch of this PR:
test_02_layed_out

File before auto-layout: test_02.drawio

See the parent attribute of the edge e2 of each .drawio file.

Modified files

  • .github/workflows/check-skill-sync.yml: Updated to check the Python script fix_edge_parents.py synced in addition to SKILL.md.
  • .github/workflows/test-scripts.yml: (New file) A workflow to run tests (plugins/tests) for fix_edge_parents.py.
  • .gitignore: Added __pycache__ and *.pyc.
  • CLAUDE.md: Added descriptions regarding fix_edge_parents.py and its tests.
  • plugins/README.md: Added descriptions regarding the tests directory.
  • plugins/claude-code/DEVELOPING.md: Added an explanation for fix_edge_parents.py.
  • plugins/claude-code/README.md: Added a step to run fix_edge_parents.py to "How It Works".
  • plugins/claude-code/skills/drawio/SKILL.md: Added an instruction to run fix_edge_parents.py after generating diagrams, and an explanation regarding the parent attribute of edges. Also added instructions in "Troubleshooting" to run fix_edge_parents.py when the edge layout is broken.
  • plugins/claude-code/skills/drawio/scripts/fix_edge_parents.py: (New file) The script to fix the parent attribute of edges.
  • plugins/codex/: Same as claude-code.
  • plugins/copilot/: Same as claude-code.
  • plugins/tests/: (New directory) The tests for fix_edge_parents.py.

Why Python?

It has an XML library in the standard library. I chose Python 3.9 to support macOS.

Why byte-level editing?

  • To minimize the diff between the agent's context and the actual file.
  • To minimize the diff from the original files when the user instructs modification of existing files.

Steps to reproduce the sample diagram

  1. mkdir tmp
    cd tmp
    claude --plugin-dir /path/to/drawio-mcp/plugins/claude-code/
  2. Type the following prompt:

    /drawio:drawio Generate a draw.io file named `test.drawio`. No AWS icon.
    
    Resources and groups:
    
    - User
    - AWS
      - VPC
        - Public Subnet
          - ALB
        - Private Subnet
          - ECS
    
    Edges:
    
    User → ALB → ECS
    
  3. drawio -x -f png -e -b 10 --layout verticalFlow -o test.png test.drawio

About use of coding agent

I used Claude Code but I reviewed the every lines of generated files and revised them thoroughly. I also revised the documentation and comments, but since I'm a non-native English speaker, they might still sound a bit AI-generated.

draw.io expects an edge's parent to be the nearest common ancestor of its
source and target cells, but LLM-authored XML (and Mermaid subgraph
conversion) leaves every edge on the default layer. Such a file renders
correctly, but ELK/--layout lays it out wrongly.

skills/drawio/scripts/fix_edge_parents.py is a transcription-close port of
mxGraphModel.updateEdgeParent as bundled with draw.io v31.1.8. It modifies
only byte ranges of affected attributes and keeps original files as much as
possible. Both a bare mxGraphModel and an mxfile are handled. Compressed
pages are handled recursively. Supports only UTF-8 and other ASCII-
compatible encodings; UTF-16/32 is refused. Only the Python 3.9 standard
library is used.

SKILL.md is updated to run the script after generating diagrams, before
running drawio --layout. claude-code, codex, copilot directories have
identical copies of the script. check-skill-sync.yml now covers the script.

plugins/tests/ is a stdlib unittest suite for the script: the parenting
rule, geometry translation, file formats and encodings, and the CLI.
test-scripts.yml runs it on Python 3.9 and 3.13.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@taku0

taku0 commented Aug 22, 2026

Copy link
Copy Markdown
Author

Since this pull request is rather big and the script is non-trivial, feel free to close this and reimplement it yourself if that's easier.

alderg added a commit that referenced this pull request Sep 18, 2026
An mxGraph edge belongs to the nearest common ancestor of its terminals,
and the editor's model keeps it that way: every edit runs
mxGraphModel.updateEdgeParents. XML written by an LLM never does - it parks
every edge on the layer, which is what our own XML reference asks for
because it renders correctly and keeps the prompt simple.

Renders correctly, lays out wrong. ELK reads an edge's coordinates in the
frame of the node that contains it, so an edge filed on the layer while both
its terminals sit inside a container comes back routed in the layer's frame
and the connector jumps out of its container (#64, reported
with a repro by taku0).

Fixing that inside the layout would mean a layout run rewriting the cell
hierarchy as a side effect, which is not something a layout may do with
default settings. Normalizing the diagram before any pass touches it is, and
it is exactly what the editor would have done to the same file:

- mx-model.js moves to shared/ and grows the rest of the mxGraphModel slice
  updateEdgeParent needs - getRoot, getOrigin, isAncestor,
  getNearestCommonAncestor, add, mxGeometry.translate, updateEdgeParents -
  ported from drawio-dev, including draw.io's ignoreRelativeEdgeParent=false.
  setGeometry now compares by value, so a write that changes nothing (a
  translated clone, a converged layout re-run) leaves the element alone.
- mx-xml.js is the XML <-> model layer lifted out of elk-pass.js, now shared
  by both passes: transformPages(xml, fn) parses each page, hands the
  transform a graph, and writes back ONLY the cells it changed.
- edge-parents.js is the pass itself, and is where the reasoning lives. It
  needs no layout engine, is idempotent, and on taku0's test_01.drawio
  rewrites exactly one attribute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alderg added a commit that referenced this pull request Sep 18, 2026
The plugins write .drawio files themselves and hand them to the desktop
CLI's --layout, so the MCP's edge-parent normalization never sees them: for
that path the rule has to land in the prompt. It did not.

Verified by running the skill headlessly (claude -p --plugin-dir) on taku0's
repro from #64 and two more container diagrams, then laying the results out
with the desktop CLI:

- Today's published skill parks every edge on the layer, reproducing #64.
- With the rule only in the XML reference's container section, two of three
  cases came out right. The third missed it because the agent skimmed the
  reference (grep -A 40 "container", sed -n 180,240p) and the rule sat at
  line 245.

So it now also sits in the general principles near the top, and in SKILL.md
itself — the skill body is always loaded in full, the reference is fetched
and often skimmed. The rule is the real one, not the old simplification:
the innermost container holding BOTH endpoints, at any nesting depth, which
for two cells in different zones of one region is that region, not the
layer.

After that all three cases came out canonical - `normalizeEdgeParents`
reports zero changes on each - and the laid-out diagrams keep every
connector inside its container.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@alderg

alderg commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Thanks for this — the repro made it easy to verify, and your diagnosis was exactly right: ELK reads an edge's coordinates in the frame of the cell that contains it, so an edge filed on the layer while both terminals sit inside one container gets laid out in the wrong frame.

We ended up taking a different route for the fix, for two reasons: we'd rather not ship a second implementation of mxGraphModel.updateEdgeParent next to the editor's own, and the plugins aren't the only path with this problem — the MCP servers generate the same kind of XML.

What's in now:

  • Both MCP servers normalize every XML diagram before anything else touches it. Edges are filed at the nearest common ancestor of their terminals using the editor's own updateEdgeParents, ported into the small headless mxGraph model the servers already carry for their ELK layout pass. Two more repairs ride along: an edge written without an <mxGeometry> (draw.io renders nothing at all for it) gets the standard one, and a container that would clip a child is grown to fit — grow-only, no child moved. This is live on mcp.draw.io and goes out with the next @drawio/mcp release.

  • draw.io Desktop gets a --normalize flag that applies the same three repairs to the model before export or open, so --normalize --layout verticalFlow is the combination the plugins will use once it ships. Checked against your test_01.drawio: it then lays out exactly like your hand-corrected test_02.

  • The shared XML reference and the skill now state the rule itself — an edge belongs to the innermost container holding both of its endpoints — so generated files are correct to begin with, with no post-processing step. We verified that by running the skill headlessly on your prompt plus two more container diagrams; all three came out canonical.

So we're going to close this PR in favour of those changes — not because the approach was wrong, but because the same algorithm now runs in draw.io's own code on every path. Thank you for the careful write-up and the test files; they're what made this quick to verify and fix.

@alderg alderg closed this Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants