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..ef84f2cde 100644 --- a/graphix/visualization.py +++ b/graphix/visualization.py @@ -215,8 +215,6 @@ def from_opengraph( ------- GraphVisualizer """ - if not og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) pos = _compute_positions(og) pos = _scale_positions(pos, options.node_distance) @@ -247,8 +245,6 @@ def from_flow( ------- GraphVisualizer """ - if not flow.og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) pos = _compute_positions(flow) pos = _scale_positions(pos, options.node_distance) @@ -284,8 +280,6 @@ def from_xzcorrections( ------- GraphVisualizer """ - if not xz_corr.og.graph.nodes: - raise ValueError("Open graph is empty.") options = VisualizationOptions(**kwargs) pos = _compute_positions(xz_corr) pos = _scale_positions(pos, options.node_distance) @@ -351,6 +345,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 +564,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( [], [], @@ -641,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)) 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")