From 63f1412cd979fd717cec1429ce00dce57e86812c Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 13 Jul 2026 13:13:49 +0200 Subject: [PATCH 1/2] Handle visualization of empty graphs --- CHANGELOG.md | 2 ++ graphix/visualization.py | 36 ++++++++++++++++++++++++++++++------ tests/test_visualization.py | 7 +++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0f40251f..861a3bb99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -59,6 +59,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Empty outputs layer removed from flow's partial order. - Flow well-formedness check does not trigger false negative for flows on open graphs without outputs. +- #562: Fixed #553. Handle visualization of empty graphs. + ### Changed - #452: Use `uv` for dependency management diff --git a/graphix/visualization.py b/graphix/visualization.py index 1212e6023..869c9930b 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -215,9 +215,15 @@ def from_opengraph( ------- GraphVisualizer """ - if not og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) + if not og.graph.nodes: + return GraphVisualizer( + og=og, + pos={}, + edge_paths={}, + options=options, + _source=_Source.OG, + ) pos = _compute_positions(og) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(og, pos) @@ -247,9 +253,15 @@ def from_flow( ------- GraphVisualizer """ - if not flow.og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) + if not flow.og.graph.nodes: + return GraphVisualizer( + og=flow.og, + pos={}, + edge_paths={}, + options=options, + _source=_Source.Flow, + ) pos = _compute_positions(flow) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(flow.og, pos) @@ -284,9 +296,15 @@ def from_xzcorrections( ------- GraphVisualizer """ - if not xz_corr.og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) + if not xz_corr.og.graph.nodes: + return GraphVisualizer( + og=xz_corr.og, + pos={}, + edge_paths={}, + options=options, + _source=_Source.XZCorr, + ) pos = _compute_positions(xz_corr) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(xz_corr.og, pos) @@ -351,6 +369,10 @@ def _determine_figsize(self) -> _Point: x_pos.add(self.pos[node][0]) y_pos.add(self.pos[node][1]) + # Empty graphs + if not x_pos or not y_pos: + return (2.0, 2.0) + width = len(x_pos) * 0.8 height = len(y_pos) return (width * self.options.node_distance[0], height * self.options.node_distance[1]) @@ -566,6 +588,8 @@ def _draw_legend(self) -> None: Legend is customized depending on the object being plotted (flow or XZ-corections) indicated in ``self._source``. """ + if not self.og.graph.nodes: + return plt.scatter( [], [], diff --git a/tests/test_visualization.py b/tests/test_visualization.py index 598082bc8..a386e936b 100644 --- a/tests/test_visualization.py +++ b/tests/test_visualization.py @@ -199,6 +199,13 @@ def test_og() -> None: pattern.draw(annotations=None) +@pytest.mark.usefixtures("mock_plot") +@pytest.mark.parametrize("annotations", [None, DrawPatternAnnotations.Flow, DrawPatternAnnotations.XZCorrections]) +def test_empty(annotations: DrawPatternAnnotations | None) -> None: + pattern = Pattern() + pattern.draw(annotations=annotations) + + # Compare with baseline/test_draw_graph_reference.png # Update baseline by running: pytest --mpl-generate-path=tests/baseline @pytest.mark.usefixtures("mock_plot") From 9daa53ab26d376888c54e8b9e1dd330bccce933e Mon Sep 17 00:00:00 2001 From: matulni Date: Mon, 13 Jul 2026 15:23:17 +0200 Subject: [PATCH 2/2] Apply suggestions Thierry's review --- graphix/visualization.py | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/graphix/visualization.py b/graphix/visualization.py index 869c9930b..ef84f2cde 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -216,14 +216,6 @@ def from_opengraph( GraphVisualizer """ options = VisualizationOptions(**kwargs) - if not og.graph.nodes: - return GraphVisualizer( - og=og, - pos={}, - edge_paths={}, - options=options, - _source=_Source.OG, - ) pos = _compute_positions(og) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(og, pos) @@ -254,14 +246,6 @@ def from_flow( GraphVisualizer """ options = VisualizationOptions(**kwargs) - if not flow.og.graph.nodes: - return GraphVisualizer( - og=flow.og, - pos={}, - edge_paths={}, - options=options, - _source=_Source.Flow, - ) pos = _compute_positions(flow) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(flow.og, pos) @@ -297,14 +281,6 @@ def from_xzcorrections( GraphVisualizer """ options = VisualizationOptions(**kwargs) - if not xz_corr.og.graph.nodes: - return GraphVisualizer( - og=xz_corr.og, - pos={}, - edge_paths={}, - options=options, - _source=_Source.XZCorr, - ) pos = _compute_positions(xz_corr) pos = _scale_positions(pos, options.node_distance) edge_paths = _compute_edge_paths(xz_corr.og, pos) @@ -665,6 +641,8 @@ def _(obj: OpenGraph[AbstractMeasurement]) -> dict[int, _Point]: X-coordinates represent the layer in the partial order (higher x = earlier layer). Y-coordinates represent the vertical position within start node chains. """ + if not obj.graph.nodes: + return {} layers: dict[int, int] = {} connected_components = list(nx.connected_components(obj.graph))