Skip to content

FEAT: add Mermaid graph rendering - #347

Open
Flomber wants to merge 36 commits into
mainfrom
Mermaidrendering
Open

FEAT: add Mermaid graph rendering#347
Flomber wants to merge 36 commits into
mainfrom
Mermaidrendering

Conversation

@Flomber

@Flomber Flomber commented Aug 18, 2026

Copy link
Copy Markdown

Closes #159

✨ New features

  • New qrules.io.asmermaid() function that renders Topology, MutableTransition, ReactionInfo, ProblemSet, QNProblemSet, and QNResult objects as Mermaid flowchart source. It mirrors the signature of asdot(), including strip_spin, collapse_graphs, the render_*_id flags, and figure_style/edge_style/node_style for styling the diagram, its edges, and its nodes.
  • qrules.io.write() now recognizes the .mmd file extension and writes Mermaid source, either from a graph-like object or from a string produced by asmermaid().
  • The documentation now renders Mermaid diagrams natively: sphinxcontrib-mermaid is enabled in docs/conf.py and docs/usage/visualize.ipynb shows Mermaid examples next to the existing Graphviz ones.

🐛 Bug fixes

  • Rule priorities in ProblemSet and QNProblemSet labels were sorted as strings, so 9 sorted above 100 and rules without a priority (NA) ended up at the top. Priorities are now parsed as numbers (negative values included) and NA sorts last.
  • collapse_graphs() iterated over its input twice, which silently produced an empty result when given a generator instead of a list. The input is now materialized first.
  • strip_projections() contained a dead freeze() branch whose result was immediately overwritten.

🔨 Maintenance

  • Label rendering that is shared between the Graphviz and Mermaid printers — as_string(), create_edge_label(), collapse_graphs(), get_particle_graphs(), and strip_projections() — moved out of qrules.io._dot into a new qrules.io._labels module, so both backends produce identical labels from a single implementation.

Squash commit messages

* DOC: show Mermaid examples in visualize notebook
* FEAT: write Mermaid source to `.mmd` files
* FIX: consume iterators in `collapse_graphs()`
* FIX: sort rule priorities numerically
* MAINT: move shared label rendering to `qrules.io._labels`

@Flomber
Flomber requested a review from redeboer August 18, 2026 12:22
@Flomber Flomber added the ✨ Feature New feature added to the package label Aug 18, 2026
Flomber and others added 6 commits August 18, 2026 14:35
…mermaid in Jupyter notebooks

- Updated the visualize_mermaid.ipynb to use asmermaid for generating Mermaid source directly.
- Removed show_mermaid_markdown function and integrated its functionality into asmermaid.
- Adjusted documentation and examples to reflect the new usage of asmermaid.
- Cleaned up the io module by consolidating Mermaid-related functions and removing unnecessary code.
- Enhanced tests to verify the new asmermaid functionality and removed tests related to the deprecated show_mermaid_markdown.
@redeboer redeboer changed the title Add Mermaid graph rendering parallel to Graphvis rendering including … FEAT: add Mermaid graph rendering Aug 19, 2026
redeboer

This comment was marked as resolved.

redeboer

This comment was marked as resolved.

Comment thread docs/usage/visualize.ipynb
Comment thread docs/usage.ipynb Outdated
Comment thread docs/usage/visualize_mermaid.ipynb
@Flomber

Flomber commented Aug 20, 2026

Copy link
Copy Markdown
Author

Found issue with rendering long lines eg. in graph edges.

isospin_magnitude ∊ [0, 1/2, 1, 3/2 ]

gets displayed as

isospin_magnitude ∊ [0, 1/2, 1, 3/2

@redeboer redeboer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good job! 💯
Some smaller comments below.

One general thought I had: it seems that Mermaid (as opposed to Graphviz) supports LaTeX, even if not for all backends. Rewriting the example of #159:

```mermaid
graph LR
    A["$$J/\psi$$"] --> N0[ ]
    N0 --> N1["$$f_0(980)$$"]
    N0 --> 0[$$\gamma$$]
    N1 --> 1[$$\pi^0$$]
    N1 --> 2[$$\pi^0$$]
    style A fill:#FFFFFF, stroke:#FFFFFF;
    style N0 fill:#FFFFFF, stroke:#FFFFFF;
    style N1 fill:#FFFFFF, stroke:#FFFFFF;
    style 0 fill:#FFFFFF, stroke:#FFFFFF;
    style 1 fill:#FFFFFF, stroke:#FFFFFF;
    style 2 fill:#FFFFFF, stroke:#FFFFFF;
```

Maybe worth posting a follow-up issue. Similarly to #348, one could select LaTeX as rendering with a flag, for instance.

Rendering on Sphinx Image
Rendering on VS Code Jupyter Image
Rendering on Jupyter Lab Image
Rendering on GitHub ❌
graph LR
    A["$$J/\psi$$"] --> N0[ ]
    N0 --> N1["$$f_0(980)$$"]
    N0 --> 0[$$\gamma$$]
    N1 --> 1[$$\pi^0$$]
    N1 --> 2[$$\pi^0$$]
    style A fill:#FFFFFF, stroke:#FFFFFF;
    style N0 fill:#FFFFFF, stroke:#FFFFFF;
    style N1 fill:#FFFFFF, stroke:#FFFFFF;
    style 0 fill:#FFFFFF, stroke:#FFFFFF;
    style 1 fill:#FFFFFF, stroke:#FFFFFF;
    style 2 fill:#FFFFFF, stroke:#FFFFFF;
Loading

Comment thread pyproject.toml Outdated
Comment thread .cspell.json Outdated
Comment thread docs/usage/visualize.ipynb
Comment thread docs/usage/visualize.ipynb
Comment thread docs/usage/visualize_mermaid.ipynb
Comment thread src/qrules/io/_mermaid.py
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py
Comment thread src/qrules/io/_mermaid.py Outdated
Comment on lines +317 to +338
def __init__(
self,
*,
render_node: bool | None = None,
render_final_state_id: bool = True,
render_resonance_id: bool = False,
render_initial_state_id: bool = False,
strip_spin: bool = False,
collapse_graphs: bool = False,
figure_style: dict[str, Any] | None = None,
edge_style: dict[str, Any] | None = None,
node_style: dict[str, Any] | None = None,
) -> None:
self.render_node = render_node
self.render_final_state_id = render_final_state_id
self.render_resonance_id = render_resonance_id
self.render_initial_state_id = render_initial_state_id
self.strip_spin = strip_spin
self.collapse_graphs = collapse_graphs
self.figure_style = dict(figure_style) if figure_style else {}
self.edge_style = dict(edge_style) if edge_style else {}
self.node_style = dict(node_style) if node_style else {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Better to declare this class with @attrs.define to reduce boilerplate code, just like GraphPrinter, which is declared with @define(on_setattr=_check_booleans).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this definition satisfies this?
Also AI mentioned, that in the definition of the GraphvizPrinter class has the weakness, that it also accepts invalid constructor combinations.
Should this also be fixed? A shared attrs validator was a recommendation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this definition satisfies this?

Yes, looks good!

Also AI mentioned, that in the definition of the GraphvizPrinter class has the weakness, that it also accepts invalid constructor combinations.

You mean that GraphPrinter does not set kw_only=True? Or because its fields have different validators/converts?

Since it makes sense to rename GraphPrinter to GraphvizPrinter now, I am considering if this is something of a larger issue that should be handled separately. But these modules are private (due to the underscore), so renaming GraphPrinter is technically not a breaking change and fits well with the intentions of rest of this PR (an extension of the io module with a new printer).

So yeah, I think it's a good idea to do that additional cleanup in this PR.

Comment thread src/qrules/io/_mermaid.py
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py Outdated
Comment thread src/qrules/io/_mermaid.py Outdated
@ComPWA ComPWA deleted a comment from review-notebook-app Bot Aug 21, 2026
@Flomber

Flomber commented Aug 21, 2026

Copy link
Copy Markdown
Author

Except for checking the width of the edge labels, the issue might be ready to be closed. 🎊
I also tried to make the label boxes transparent, but this didn't quiet work out, so I stoped trying 😞

@redeboer redeboer added 🔨 Maintenance Maintenance and upkeep improvements 🐛 Bug Something isn't working labels Aug 21, 2026

@redeboer redeboer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! But let's discuss your comment #347 (comment) about the GraphPrinter class next week. It may be worth fixing that in this PR too.

Comment thread src/qrules/io/_mermaid.py Outdated
Comment on lines +317 to +338
def __init__(
self,
*,
render_node: bool | None = None,
render_final_state_id: bool = True,
render_resonance_id: bool = False,
render_initial_state_id: bool = False,
strip_spin: bool = False,
collapse_graphs: bool = False,
figure_style: dict[str, Any] | None = None,
edge_style: dict[str, Any] | None = None,
node_style: dict[str, Any] | None = None,
) -> None:
self.render_node = render_node
self.render_final_state_id = render_final_state_id
self.render_resonance_id = render_resonance_id
self.render_initial_state_id = render_initial_state_id
self.strip_spin = strip_spin
self.collapse_graphs = collapse_graphs
self.figure_style = dict(figure_style) if figure_style else {}
self.edge_style = dict(edge_style) if edge_style else {}
self.node_style = dict(node_style) if node_style else {}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this definition satisfies this?

Yes, looks good!

Also AI mentioned, that in the definition of the GraphvizPrinter class has the weakness, that it also accepts invalid constructor combinations.

You mean that GraphPrinter does not set kw_only=True? Or because its fields have different validators/converts?

Since it makes sense to rename GraphPrinter to GraphvizPrinter now, I am considering if this is something of a larger issue that should be handled separately. But these modules are private (due to the underscore), so renaming GraphPrinter is technically not a breaking change and fits well with the intentions of rest of this PR (an extension of the io module with a new printer).

So yeah, I think it's a good idea to do that additional cleanup in this PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 Bug Something isn't working ✨ Feature New feature added to the package 🔨 Maintenance Maintenance and upkeep improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Render transitions as Mermaid

3 participants