Skip to content

Module: plot

File: src/spatial_graph_algorithms/plot/ Status: Stable.


Purpose

Visualise SpatialGraph objects. All functions return a matplotlib.figure.Figure and optionally save it to disk. No algorithm logic lives here — only rendering.


Functions

Function Input requirements What it shows
plot_network 2-D positions Nodes + edges; false edges in solid red
plot_network_3d 3-D positions 3-D scatter + edges with adaptive sampling
plot_edge_length_histogram positions + edges Distribution of Euclidean edge lengths
plot_comparison positions + reconstructed_positions (2-D) Original vs reconstructed side-by-side
plot_provenance_comparison positions + reconstructed_positions (2-D) Original vs reconstructed with spatial pattern overlay
render_simulation_visualization_bundle SpatialGraph Saves network + histogram to disk

plot_comparison in Detail

This is the most complex plot. It:

  1. Applies Procrustes alignment (scipy.spatial.procrustes) to the reconstructed positions. This standardizes both arrays to unit Frobenius norm, then finds optimal rotation + reflection, so orientation differences do not obscure quality.
  2. Colours nodes by angle from centroid in the original space. The same colour in both panels means the same node — so a good reconstruction shows the same colour gradient.
  3. Uses the same colour range on both panels for a fair comparison.

The Procrustes alignment is applied only for visualisation — it is not applied to sg.reconstructed_positions stored in the object.


plot_provenance_comparison in Detail

This function assigns a color to every node based on where it sits in the ground-truth coordinate space, then renders that same color in both panels.

Available patterns (pass as pattern=):

kind What it shows Key params
"grid" (default) nx×ny colored spatial grid — like a colored checkerboard grid_size, cmap
"checkerboard" Classic two-color alternating grid tile_count, color_a, color_b
"rings" Concentric color rings from centroid n_rings, cmap
"quadrants" Angular color wedges from centroid n_wedges, cmap
"gradient" Smooth axis-aligned color gradient direction, cmap
"image" RGB colors sampled from a user-supplied PNG image_path

Key design decision: colors are always derived from sg.positions (ground truth) only, never from reconstructed_positions. This means node i has the same color in both panels regardless of where the reconstruction placed it. A good reconstruction shows the pattern intact; a poor one scrambles or distorts it.


Design Decisions

How are large graph network plots kept readable? plot_network and plot_network_3d use adaptive defaults based on edge count: small and medium graphs draw all edges, while larger graphs sample edges in edge_display="auto" mode. Node size and opacity are reduced as the graph gets larger, and dense plots are rasterized so saved files stay usable. False edges are preserved before true edges are sampled so noisy-edge diagnostics remain visible. Pass edge_display="all" to force every edge, edge_display="sample" with max_edges=... for an explicit budget, or edge_display="none" for a node-only view.

Why are false edges solid red? False edges are diagnostic overlays, so they are drawn after true edges with a solid red stroke. Their opacity is high when only a few false edges are drawn and automatically reduced when many are visible, which avoids turning noisy medium graphs into solid red overlays.

Why does DEFAULT_VISUALIZATION_DIR point to .planning/artifacts/visualizations? Historical — that was the GSD planning artefact path. For user-facing output, pass your own output_dir argument. This default may change in a future version.

Why return Figure instead of showing it? Returning the figure lets callers decide whether to display (plt.show()), save (fig.savefig()), or embed in a notebook. Functions that call plt.show() inside are harder to test and less flexible.

Why not use seaborn or plotly? matplotlib is already a core dependency. Adding seaborn or plotly for this module would add installation overhead for optional visualisation niceties.


How to Add a New Plot

  1. Add a function in plot/network.py with this signature pattern:
    def plot_<name>(
        sg: SpatialGraph,
        *,
        figsize: tuple[float, float] = (6.0, 4.5),
        save: bool = False,
        output_dir: str | Path = DEFAULT_VISUALIZATION_DIR,
        filename: str = "<name>.png",
    ) -> Figure:
        """One-line description."""
        if sg.positions is None:
            raise ValueError("...")
        ...
        return fig
    
  2. Export it in plot/__init__.py and add to __all__.
  3. Add a test that asserts the return type is matplotlib.figure.Figure.
  4. Close the figure in tests: import matplotlib.pyplot as plt; plt.close(fig).

Rules: - Always validate inputs at the top and raise ValueError with a clear message. - Never call plt.show() inside a plot function. - Never call plt.close() inside a plot function — let the caller manage figure lifecycle. - Always use dpi=300 for saved outputs.


Tests

tests/test_plot_simulation.py   — 2D plots, bundle, DEFAULT_VISUALIZATION_DIR constant
tests/test_plot_3d.py           — 3D plot
tests/test_plot_provenance.py   — apply_pattern and plot_provenance_comparison