Skip to content

plot_coherence_matrix: split image and matrix windows#1493

Merged
yunjunz merged 14 commits into
insarlab:mainfrom
huchangyang:feature/coherence-matrix-split-windows-clean
May 29, 2026
Merged

plot_coherence_matrix: split image and matrix windows#1493
yunjunz merged 14 commits into
insarlab:mainfrom
huchangyang:feature/coherence-matrix-split-windows-clean

Conversation

@huchangyang
Copy link
Copy Markdown
Contributor

@huchangyang huchangyang commented May 25, 2026

Description of proposed changes

This PR separates the interactive coherence matrix viewer into two matplotlib windows: one for the reference image/map and one for the coherence matrix.

image

The previous layout placed both views in a single figure, which made the coherence matrix small for large interferogram networks. With this change, clicking a pixel in the image window still updates the coherence matrix, while the selected pixel marker is refreshed in the image window.

This PR does not change the coherence matrix calculation or add new plotting modes. It only reorganizes the interactive viewer layout to make the matrix easier to inspect.

Reminders

  • Fix #xxxx
  • Pass Pre-commit check (green)
  • Pass Codacy code review (green)
  • Pass Circle CI test (green)
  • Make sure that your code follows our style. Use the other functions/files as a basis.
  • If modifying functionality, describe changes to function behavior and arguments in a comment below the function declaration.
  • If adding new functionality, add a detailed description to the documentation and/or an example.

Summary by Sourcery

Split the coherence matrix viewer into separate image and matrix windows to improve matrix visibility while preserving interactive pixel selection.

New Features:

  • Display the reference image and coherence matrix in two independent matplotlib windows linked by interactive pixel selection.

Enhancements:

  • Adjust figure sizing logic to choose appropriate dimensions for the coherence matrix window based on network size.
  • Ensure the selected pixel marker in the image view is refreshed when coherence matrix updates, improving visual feedback during interaction.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented May 25, 2026

Reviewer's Guide

Splits the interactive coherence matrix viewer into two separate matplotlib figures (image and matrix), refactors figure/axes handling accordingly, and updates click handling to keep the two windows synchronized while improving matrix visibility and image markers.

Sequence diagram for synchronized image and coherence matrix clicks

sequenceDiagram
    actor User
    participant Matplotlib as Matplotlib_canvas
    participant Viewer as coherenceMatrixViewer
    participant FigImg as fig_img
    participant FigMat as fig_mat

    User->>FigImg: mouse click
    FigImg-->>Matplotlib: button_press_event
    Matplotlib-->>Viewer: update_coherence_matrix(event)
    alt event.inaxes == ax_img
        Viewer->>Viewer: plot_coherence_matrix4pixel(yx)
        Viewer->>Viewer: update_image_marker(yx)
        Viewer->>FigMat: fig_mat.canvas.draw_idle()
        Viewer->>FigMat: fig_mat.canvas.flush_events()
        Viewer->>FigImg: fig_img.canvas.draw_idle()
    else event.inaxes == ax_mat
        Viewer->>Viewer: update_coherence_matrix(event) [pass]
    end
Loading

File-Level Changes

Change Details Files
Split single combined figure into separate image and coherence-matrix figures with independent sizing and titles.
  • Replaced single figure attributes (figname, fig_size, fig) with separate image and matrix figure attributes (figname_img/figsize_img/fig_img and figname_mat/figsize_mat/fig_mat).
  • Initialized image and matrix figures independently in plot() using plt.subplots with distinct names and sizes.
  • Assigned dedicated axes for image and matrix views (ax_img and ax_mat) corresponding to their separate figures and removed add_axes layout on a shared figure.
src/mintpy/plot_coherence_matrix.py
Introduce automatic, size-aware figure sizing for both image and coherence-matrix windows.
  • Changed auto figure sizing to compute figsize_img based on the input image shape only, using pp.auto_figure_size and logging the chosen image figure size.
  • Added logic to choose figsize_mat based on the number of interferograms (date12_list length) with tiered default sizes for small, medium, and large networks and log the matrix figure size.
  • Ensured figsize_img and figsize_mat are only computed if not already set, preserving any externally provided sizes.
src/mintpy/plot_coherence_matrix.py
Refactor initial image plotting into a helper and enhance window presentation.
  • Extracted the initial image plotting logic from plot() into a new method plot_init_image() that prepares the image data, coordinates, and axes.
  • Within plot_init_image(), set the image figure window title to the image figname and applied tight_layout() for better spacing.
  • Preserved existing behavior for view.prep_slice, coordinate handling, and view.plot_slice while reusing ax_img and fig_coord on the new image figure.
src/mintpy/plot_coherence_matrix.py
Update event handling to support two-window interaction and keep image markers in sync with matrix updates.
  • Replaced a single canvas event connection (cid on self.fig) with separate connections for image and matrix canvases (cid_img and cid_mat) both wired to update_coherence_matrix.
  • Enhanced update_coherence_matrix() to explicitly handle clicks on the image axes, compute yx from click coordinates, update the coherence matrix for that pixel, and refresh the image marker; clicks on the matrix axes are currently ignored (pass).
  • Added update_image_marker() helper that removes any existing triangle marker on the image axes and draws a new red triangle with black edge at the selected pixel, then redraws the image figure canvas.
src/mintpy/plot_coherence_matrix.py
Ensure coherence-matrix figure has appropriate window title and layout and update only that canvas when matrix changes.
  • Set the coherence-matrix figure window title and applied tight_layout() after initial matrix plotting to improve usability.
  • Changed the redraw logic in plot_coherence_matrix4pixel() to call draw_idle() and flush_events() on the matrix figure canvas (fig_mat) instead of the old combined figure (fig).
  • Retained existing coherence matrix plotting and temporal coherence reporting behavior while redirecting all drawing to the matrix window.
src/mintpy/plot_coherence_matrix.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • Since update_coherence_matrix does nothing for event.inaxes == self.ax_mat, you can avoid connecting self.fig_mat to the callback entirely (or implement a behavior for matrix clicks) to reduce unnecessary event handling.
  • The logic that sets window titles and calls tight_layout() for both figures is split between plot and plot_init_image/plot_coherence_matrix4pixel; consider centralizing this in one place per figure to avoid duplication and ensure consistent behavior.
  • In update_image_marker, removing all artists with marker '^' may unintentionally remove other markers; you could instead track the specific marker (e.g., store the Line2D returned by plot) and update/remove just that one.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Since `update_coherence_matrix` does nothing for `event.inaxes == self.ax_mat`, you can avoid connecting `self.fig_mat` to the callback entirely (or implement a behavior for matrix clicks) to reduce unnecessary event handling.
- The logic that sets window titles and calls `tight_layout()` for both figures is split between `plot` and `plot_init_image`/`plot_coherence_matrix4pixel`; consider centralizing this in one place per figure to avoid duplication and ensure consistent behavior.
- In `update_image_marker`, removing all artists with marker `'^'` may unintentionally remove other markers; you could instead track the specific marker (e.g., store the Line2D returned by `plot`) and update/remove just that one.

## Individual Comments

### Comment 1
<location path="src/mintpy/plot_coherence_matrix.py" line_range="226-227" />
<code_context>
             msg += f'temporal coherence: {tcoh:.2f}'
         vprint(msg)

+        self.fig_mat.canvas.manager.set_window_title(self.figname_mat)
+        self.fig_mat.tight_layout()
+
         # update figure
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid calling `tight_layout()` on every coherence-matrix update to prevent layout jitter and unnecessary work.

Since `plot_coherence_matrix4pixel` runs on every click, this will call `self.fig_mat.tight_layout()` each time, which is relatively expensive and can cause the matrix and colorbar to shift slightly between interactions. Consider calling `tight_layout()` once after the initial matrix creation (or guarding it with a flag) instead of on every update, unless you truly need to recompute the layout each time.

```suggestion
        self.fig_mat.canvas.manager.set_window_title(self.figname_mat)

        # call tight_layout only once to avoid jitter and repeated work
        if not hasattr(self, "_mat_tight_layout_done"):
            self.fig_mat.tight_layout()
            self._mat_tight_layout_done = True
```
</issue_to_address>

### Comment 2
<location path="src/mintpy/plot_coherence_matrix.py" line_range="250-254" />
<code_context>
+
+    def update_image_marker(self, yx):
+        """Update the marker point in the image window."""
+        for artist in self.ax_img.get_children():
+            if hasattr(artist, 'get_marker') and artist.get_marker() == '^':
+                artist.remove()
+
+        self.ax_img.plot(yx[1], yx[0], 'r^', markersize=10, markeredgecolor='black')
+        self.fig_img.canvas.draw_idle()
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Track the marker artist directly instead of scanning all children and matching by marker symbol.

Scanning all `ax_img` children and relying on `get_marker() == '^'` risks removing unrelated markers that happen to use the same symbol, and it adds per-click overhead. Instead, store a reference to the specific marker when you create it (e.g. `self._marker_artist`), remove it directly if present, and then recreate/update it. This will be more robust and more efficient.

Suggested implementation:

```python
    def update_image_marker(self, yx):
        """Update the marker point in the image window."""
        # Remove existing marker, if any
        marker = getattr(self, "_image_marker", None)
        if marker is not None:
            marker.remove()
            self._image_marker = None

        # Create and store new marker
        self._image_marker, = self.ax_img.plot(
            yx[1],
            yx[0],
            'r^',
            markersize=10,
            markeredgecolor='black',
        )
        self.fig_img.canvas.draw_idle()

```

1. In the class constructor (or an appropriate initialization method), add `self._image_marker = None` so the attribute always exists.
2. If this class is pickled or copied, ensure `_image_marker` is either excluded or correctly reset during those operations, consistent with the rest of your codebase.
</issue_to_address>

### Comment 3
<location path="src/mintpy/plot_coherence_matrix.py" line_range="168" />
<code_context>
-        self.cid = self.fig.canvas.mpl_connect('button_press_event', self.update_coherence_matrix)
-        if self.disp_fig:
-            plt.show()
+        self.fig_img.canvas.manager.set_window_title(self.figname_img)
+        self.fig_img.tight_layout()
         return
</code_context>
<issue_to_address>
**suggestion:** Guard against `canvas.manager` being `None` for non-interactive backends.

On some Matplotlib backends `self.fig_img.canvas.manager` can be `None`, which would raise an `AttributeError` here. Please guard this with a check (and do the same for the matrix figure) before calling `set_window_title`.

Suggested implementation:

```python
        manager = getattr(getattr(self.fig_img, "canvas", None), "manager", None)
        if manager is not None:
            manager.set_window_title(self.figname_img)

        self.fig_img.tight_layout()
        return

```

1. Find the code that sets the window title for the coherence matrix figure (likely something like `self.fig.canvas.manager.set_window_title(self.figname)` or similar).
2. Replace that direct call with the same guarded pattern, e.g.:

```python
manager = getattr(getattr(self.fig, "canvas", None), "manager", None)
if manager is not None:
    manager.set_window_title(self.figname)
```

This will ensure both figures behave correctly on non-interactive backends where `canvas.manager` may be `None`.
</issue_to_address>

### Comment 4
<location path="src/mintpy/plot_coherence_matrix.py" line_range="65" />
<code_context>
-        self.figname = 'Coherence matrix'
-        self.fig_size = None
-        self.fig = None
+        self.figname_img = 'Image'
+        self.figsize_img = None
+        self.fig_img = None
</code_context>
<issue_to_address>
**issue (complexity):** Consider consolidating figure-related state and behavior into shared helpers and structures so the two-window plotting logic stays DRY and easier to maintain.

You can keep the new functionality and still cut a fair amount of complexity by consolidating state and factoring out small helpers. Concretely:

### 1. Avoid scattered figure state / duplicated event wiring

Group the two figures into a small structure and iterate over them instead of keeping many parallel attributes and manual connections:

```python
def __init__(self, inps):
    # ...
    self.figures = {
        "img": {"name": "Image", "size": None, "fig": None, "ax": None},
        "mat": {"name": "Coherence Matrix", "size": None, "fig": None, "ax": None},
    }
    self._marker_artist = None
```

Then in `open()` / `plot()`:

```python
def open(self):
    # ...
    if not self.figures["img"]["size"]:
        ds_shape = readfile.read(self.img_file)[0].shape
        self.figures["img"]["size"] = pp.auto_figure_size(
            ds_shape, disp_cbar=True, scale=0.7
        )
    if not self.figures["mat"]["size"]:
        self.figures["mat"]["size"] = self._auto_matrix_figsize(len(self.date12_list))
    # ...

def plot(self):
    img_cfg = self.figures["img"]
    mat_cfg = self.figures["mat"]

    img_cfg["fig"], img_cfg["ax"] = plt.subplots(
        num=img_cfg["name"], figsize=img_cfg["size"]
    )
    mat_cfg["fig"], mat_cfg["ax"] = plt.subplots(
        num=mat_cfg["name"], figsize=mat_cfg["size"]
    )

    self.fig_img, self.ax_img = img_cfg["fig"], img_cfg["ax"]
    self.fig_mat, self.ax_mat = mat_cfg["fig"], mat_cfg["ax"]

    self._init_fig_layout()

    self.plot_init_image()
    if all(i is not None for i in self.yx):
        self.plot_coherence_matrix4pixel(self.yx)

    for fig in (self.fig_img, self.fig_mat):
        self._connect_events(fig)

    if self.disp_fig:
        plt.show()
```

Helper methods:

```python
def _connect_events(self, fig):
    fig.canvas.mpl_connect('button_press_event', self.update_coherence_matrix)

def _init_fig_layout(self):
    for cfg in (self.figures["img"], self.figures["mat"]):
        if cfg["fig"] is None:
            continue
        cfg["fig"].canvas.manager.set_window_title(cfg["name"])
        cfg["fig"].tight_layout()
```

This removes the parallel `figname_*`, `figsize_*`, `fig_*` attributes, and the duplicated `mpl_connect` calls, while keeping behavior identical.

### 2. Extract matrix figure sizing from `open()`

Move the inline size-logic into a dedicated helper:

```python
def _auto_matrix_figsize(self, num_ifg):
    if num_ifg <= 50:
        return [6, 5]
    if num_ifg <= 100:
        return [8, 6]
    return [10, 8]
```

Now `open()` just calls `_auto_matrix_figsize`, which is easier to find and change.

### 3. Centralize window title / layout calls

Instead of calling `set_window_title` and `tight_layout` in `plot_init_image` and `plot_coherence_matrix4pixel`, let them be pure “data rendering” and keep UI/layout in `_init_fig_layout()` (see above). You can then simplify these methods to only touch axes content:

```python
def plot_init_image(self):
    # ... existing data setup ...
    self.ax_img = view.plot_slice(self.ax_img, d_img, atr, view_inps)[0]
    self.fig_coord = view_inps.fig_coord

def plot_coherence_matrix4pixel(self, yx):
    self.ax_mat.cla()
    # ... existing matrix computation / imshow / text ...
    self.fig_mat.canvas.draw_idle()
    self.fig_mat.canvas.flush_events()
```

### 4. Use a single stored marker artist instead of scanning children

Replace the child-scan in `update_image_marker` with a single cached artist:

```python
def __init__(self, inps):
    # ...
    self._marker_artist = None

def update_image_marker(self, yx):
    if self._marker_artist is None:
        (self._marker_artist,) = self.ax_img.plot(
            yx[1], yx[0], 'r^', markersize=10, markeredgecolor='black'
        )
    else:
        self._marker_artist.set_data([yx[1]], [yx[0]])

    self.fig_img.canvas.draw_idle()
```

This makes the intent explicit (“there is exactly one marker”) and avoids per-click iteration over all artists.

### 5. Simplify `update_coherence_matrix` responsibilities

Keep `update_coherence_matrix` as a dispatcher and delegate to smaller helpers:

```python
def update_coherence_matrix(self, event):
    if event.inaxes == self.ax_img:
        self._handle_image_click(event)
    elif event.inaxes == self.ax_mat:
        self._handle_matrix_click(event)

def _handle_image_click(self, event):
    if self.fig_coord == 'geo':
        yx = self.coord.lalo2yx(event.ydata, event.xdata)
    else:
        yx = [int(event.ydata + 0.5), int(event.xdata + 0.5)]

    self.plot_coherence_matrix4pixel(yx)
    self.update_image_marker(yx)

def _handle_matrix_click(self, event):
    # currently no-op; keep placeholder for future behavior
    pass
```

This keeps coordinate transforms, matrix plotting, and marker updates in clearly named helpers, while preserving the behavior you added.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/mintpy/plot_coherence_matrix.py Outdated
Comment thread src/mintpy/plot_coherence_matrix.py Outdated
Comment thread src/mintpy/plot_coherence_matrix.py
Comment thread src/mintpy/plot_coherence_matrix.py
@yunjunz yunjunz changed the title plot_coherence_matrix: split image and matrix windows plot_coherence_matrix: split image and matrix windows May 27, 2026
yunjunz and others added 7 commits May 27, 2026 17:07
Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com>
+ plot init marker: replace view.plot_slice() with update_image_marker()

+ move update_image_marker() within plot_coherence_matrix4pixel() for call simplicity

+ show lat/lon info in the terminal whenever it's available
@huchangyang huchangyang force-pushed the feature/coherence-matrix-split-windows-clean branch from d37bda8 to d7c3bb8 Compare May 29, 2026 05:23
Copy link
Copy Markdown
Member

@yunjunz yunjunz left a comment

Choose a reason for hiding this comment

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

Looks all good to me now. Thank you @huchangyang!

@yunjunz yunjunz merged commit 6ba3312 into insarlab:main May 29, 2026
9 checks passed
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