Skip to content

Conversation

@AJB-BigA
Copy link
Collaborator

@AJB-BigA AJB-BigA commented Oct 16, 2025

Updated the transect tool to reflect onko main

Summary by Sourcery

Refactor the transect tool to decouple plotting logic from the Qt UI and enrich it with ROI thresholds, update slice navigation controls, and tweak UI styling

Enhancements:

  • Refactor TransectWindow into a standalone plot_transect_from_hu function using pure Matplotlib and return detailed ROI data
  • Enhance ROIOptionsController.eventFilter to support arrow key and mouse wheel events for slice scrolling
  • Increase default outline width of circle cursor in ButtonBox and remove extraneous whitespace
  • Change Canvas transect pen color to red and remove automatic display of the transect window

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Oct 16, 2025

Reviewer's Guide

This PR refactors the transect tool by replacing the embedded Qt widget plot with a standalone numpy/matplotlib implementation (plot_transect_from_hu), enhances ROI event handling for arrow and wheel interactions, and includes UI clean-ups (whitespace, cursor/pen color tweaks, removal of redundant show calls).

Sequence diagram for updated event handling in ROIOptionsController

sequenceDiagram
participant User as actor
participant ROIOptionsController
participant HUD
participant Scroller
User->>ROIOptionsController: Arrow key press (event.type() == 12)
ROIOptionsController->>HUD: setGeometry(viewport.rect())
User->>ROIOptionsController: Mouse wheel event (event.type() == Wheel)
ROIOptionsController->>HUD: setGeometry(viewport.rect())
ROIOptionsController->>Scroller: setValue(scroller.value() +1 or -1)
Loading

Class diagram for updated TransectWindow and plot_transect_from_hu

classDiagram
class TransectWindow {
    +__init__(hu_values=[])
    +plot_transect_from_hu(hu_values, pix_spacing=1.0, thresholds_mm=None, is_roi_draw=True, show=True)
}
TransectWindow : QWidget
TransectWindow --> plot_transect_from_hu : uses
Loading

Class diagram for ROIOptionsController eventFilter changes

classDiagram
class RoiDrawOptions {
    +update_ui(rois, dataset_rtss)
    +eventFilter(obj, event)
    +set_up(...)
    +signal_roi_drawn
    +signal_draw_roi_closed
    +_hud
    +view
    +scroller
}
RoiDrawOptions : QtWidgets.QWidget
RoiDrawOptions --> eventFilter : overrides
Loading

Class diagram for ButtonBox make_circle_cursor change

classDiagram
class ButtonBox {
    +make_circle_cursor(size, outline, fill, outline_width=3, hotspot_center=True) : QCursor
}
ButtonBox : QtWidgets.QWidget
Loading

Class diagram for Canvas pen color change and transect_window update

classDiagram
class Canvas {
    +__init__(pen, slider, rtss)
    +t_pen : QPen
    +transect_window(point_values)
    +canvas
    +slice_num
    +setPixmap(...)
}
Canvas : QtWidgets.QLabel
Canvas --> TransectWindow : uses
Loading

File-Level Changes

Change Details Files
Refactor TransectWindow to a standalone numpy/matplotlib plotting API
  • Replace QtFigureCanvas with matplotlib.pyplot/NumPy-based plotting
  • Add plot_transect_from_hu method with parameters for spacing, thresholds, ROI overlay, and return dict
  • Remove old Figure/Canvas setup and embedding in widget
  • Implement default threshold logic, step plot, ROI masking, and optional plt.show()
src/View/mainpage/DrawROIWindow/Transect_Window.py
Improve eventFilter in ROIOptionsController to support arrow keys and wheel scrolling
  • Add docstring for eventFilter
  • Handle arrow key event (type 12) to update HUD geometry
  • Handle Qt.Wheel events to adjust scroller value up/down
  • Ensure proper event consumption/propagation
src/Controller/ROIOptionsController.py
Clean up whitespace in ButtonBox methods
  • Remove trailing spaces
  • Standardize indentation around method definitions
src/View/mainpage/DrawROIWindow/ButtonBox.py
Adjust transect pen color and remove redundant window show in Canvas
  • Change transect pen color from custom pink to Qt.red
  • Remove t_window.show() call in transect_window
src/View/mainpage/DrawROIWindow/Canvas.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
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 there - I've reviewed your changes and they look great!

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:48` </location>
<code_context>
+            "axes": matplotlib.axes.Axes,
+            }
+        """
+        hu_values = np.asarray(hu_values).astype(float)
+        n = hu_values.size
+        if n == 0:
</code_context>

<issue_to_address>
**suggestion:** Consider handling NaN or infinite values in hu_values.

Sanitizing or warning about NaN or infinite values in hu_values before plotting will help prevent unexpected behavior in matplotlib.
</issue_to_address>

### Comment 2
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:68` </location>
<code_context>
+            left_mm, right_mm = right_mm, left_mm  # ensure left <= right
+
+        # Create figure/axes and step plot
+        fig = plt.figure(num="Transect Graph")
+        ax = fig.add_subplot(111)
+        ax.step(x_mm, hu_values, where="mid")
</code_context>

<issue_to_address>
**suggestion:** Using plt.figure in a QWidget context may cause multiple windows.

Consider using FigureCanvas to embed the matplotlib figure within the QWidget, ensuring a consistent UI and preventing unwanted additional windows.

Suggested implementation:

```python
        # Create figure/axes and step plot using FigureCanvas to embed in QWidget
        from matplotlib.figure import Figure
        from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas

        fig = Figure()
        ax = fig.add_subplot(111)
        ax.step(x_mm, hu_values, where="mid")

        # Embed the figure in the QWidget
        canvas = FigureCanvas(fig)
        # Assuming self is a QWidget and has a layout
        self.layout().addWidget(canvas)

```

- If the imports for `Figure` and `FigureCanvasQTAgg` are not already present at the top of the file, add:
  ```python
  from matplotlib.figure import Figure
  from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
  ```
- Ensure that `self` refers to a QWidget and has a layout set (e.g., `QVBoxLayout`). If not, you may need to initialize a layout for the widget.
- If you want to update the plot multiple times, consider removing previous canvases from the layout before adding a new one.
</issue_to_address>

### Comment 3
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:85` </location>
<code_context>
+            roi_x_mm = x_mm[mask]
+            roi_hu = hu_values[mask]
+
+        if show:
+            plt.show(block = False)
+
</code_context>

<issue_to_address>
**issue (bug_risk):** Calling plt.show(block=False) may interfere with Qt event loop.

Consider integrating matplotlib with the Qt event loop or using FigureCanvas instead of plt.show(block=False) to avoid event loop conflicts.
</issue_to_address>

### Comment 4
<location> `src/Controller/ROIOptionsController.py:79-83` </location>
<code_context>
             self._hud.setGeometry(self.view.viewport().rect())
             return False  # don't consume; just reacting
-        if obj is self.view.viewport() and event.type() == QtCore.QEvent.Wheel:
+        #12 is the  number acosiated with the arrow press event
+        #will trigger when the arrow key is pressed
+        if event.type() == 12:
             self._hud.setGeometry(self.view.viewport().rect())
-            return False  # don't consume; just reacting
</code_context>

<issue_to_address>
**suggestion:** Hardcoding event type as 12 reduces code clarity and maintainability.

Use the relevant QtCore.QEvent enum value for arrow key events instead of the number 12 to enhance code readability and maintainability.

```suggestion
        # Handle key press events (e.g., arrow keys) using the QtCore.QEvent.KeyPress enum for clarity
        if event.type() == QtCore.QEvent.KeyPress:
            self._hud.setGeometry(self.view.viewport().rect())
            return False
```
</issue_to_address>

### Comment 5
<location> `src/View/mainpage/DrawROIWindow/Canvas.py:282` </location>
<code_context>


         self.t_window = TransectWindow(transected_values)
-        self.t_window.show()
         self.canvas[self.slice_num] = self.transect_pixmap_copy
         self.setPixmap(self.canvas[self.slice_num])
</code_context>

<issue_to_address>
**question (bug_risk):** Removing t_window.show() may prevent the window from appearing.

Confirm that TransectWindow is displayed as needed, either through alternative logic or within its own implementation.
</issue_to_address>

### Comment 6
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:11` </location>
<code_context>

-        self.figure = Figure()
-        self.canvas = FigureCanvas(self.figure)
+    def plot_transect_from_hu(self, 
+    hu_values,
+    pix_spacing=1.0,
</code_context>

<issue_to_address>
**issue (complexity):** Consider splitting the data preparation and plotting logic in plot_transect_from_hu into separate helper functions for clarity and testability.

```markdown
I’d split `plot_transect_from_hu` into two focused helpers—one for preparing the data (x_mm, thresholds and mask) and one for rendering—so each part is easy to read and test.

```python
# helper #1: data preparation
def _prepare_transect_data(hu_values, pix_spacing=1.0, thresholds_mm=None):
    hu = np.asarray(hu_values, float)
    if hu.size == 0:
        raise ValueError("hu_values is empty.")
    x_mm = np.arange(hu.size) * pix_spacing

    # default thresholds and sort
    if thresholds_mm is None:
        thresholds_mm = (x_mm[0], x_mm[-1]) if hu.size > 1 else (x_mm[0], x_mm[0])
    left, right = sorted(thresholds_mm)

    mask = (x_mm >= left) & (x_mm <= right)
    return x_mm, hu, (left, right), mask
```

```python
# helper #2: plotting onto a given Figure/Axes
def _draw_transect(ax, x_mm, hu, thresholds, mask, is_roi_draw):
    ax.step(x_mm, hu, where="mid")
    ax.set(xlabel="Distance (mm)", ylabel="CT #", title="Transect Graph")
    ax.grid(True)
    if is_roi_draw:
        left, right = thresholds
        ax.axvline(left, color="r")
        ax.axvline(right, color="r")
    return ax
```

And then simplify your main method:

```python
def plot_transect_from_hu(...):
    x_mm, hu, thresholds, mask = self._prepare_transect_data(
        hu_values, pix_spacing, thresholds_mm
    )
    fig = Figure()
    ax = fig.add_subplot(111)
    self._draw_transect(ax, x_mm, hu, thresholds, mask, is_roi_draw)
    if show:
        plt.show(block=False)
    return {
        "x_mm": x_mm,
        "hu_values": hu,
        "thresholds_mm": thresholds,
        "roi_x_mm": x_mm[mask] if is_roi_draw else np.array([]),
        "roi_hu": hu[mask] if is_roi_draw else np.array([]),
        "figure": fig,
        "axes": ax,
    }
```

This keeps the same behavior but clearly separates data logic from plotting, reduces nesting, and avoids scattering NumPy conversions and threshold logic inside the big method.
</issue_to_address>

### Comment 7
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:7` </location>
<code_context>
    def __init__(self, hu_values = []):
        super().__init__()
        self.plot_transect_from_hu(hu_values= hu_values)

</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace mutable default arguments with None ([`default-mutable-arg`](https://docs.sourcery.ai/Reference/Default-Rules/suggestions/default-mutable-arg/))

```suggestion
    def __init__(self, hu_values=None):
        if hu_values is None:
            hu_values = []
```
</issue_to_address>

### Comment 8
<location> `src/View/mainpage/DrawROIWindow/Transect_Window.py:58-62` </location>
<code_context>
    def plot_transect_from_hu(self, 
    hu_values,
    pix_spacing=1.0,
    thresholds_mm=None,     # (left_mm, right_mm) along the transect; if None -> (second sample, last sample)
    is_roi_draw=True,
    show=True,
    ):
        """
        Plot a transect (HU vs distance in mm) from an array of HU values.

        Parameters
        ----------
        hu_values : array-like
            1D sequence of HU samples along a transect.
        pix_spacing : float, optional
            Physical spacing per sample (mm per sample). Default 1.0.
        thresholds_mm : tuple(float, float) or None, optional
            (left_mm, right_mm) bounds for the ROI along the x-axis in millimetres.
            If None, defaults to (x[1], x[-1]) when there are >= 2 points, otherwise (x[0], x[0]).
        is_roi_draw : bool, optional
            If True, draw red threshold lines and compute ROI points.
        show : bool, optional
            If True, call plt.show() at the end.

        Returns
        -------
        result : dict
            {
            "x_mm": np.ndarray,            # x positions in mm
            "hu_values": np.ndarray,       # original HU values as np.ndarray
            "thresholds_mm": (float, float),
            "roi_x_mm": np.ndarray,        # x positions inside ROI (empty if is_roi_draw=False)
            "roi_hu": np.ndarray,          # HU values inside ROI (empty if is_roi_draw=False)
            "figure": matplotlib.figure.Figure,
            "axes": matplotlib.axes.Axes,
            }
        """
        hu_values = np.asarray(hu_values).astype(float)
        n = hu_values.size
        if n == 0:
            raise ValueError("hu_values is empty.")

        # Build x-axis in millimetres from index * spacing
        x_mm = np.arange(n, dtype=float) * float(pix_spacing)

        # Choose default thresholds if none provided
        if thresholds_mm is None:
            if n == 1:
                thresholds_mm = (x_mm[0], x_mm[0])
            else:
                thresholds_mm = (x_mm[0], x_mm[-1])

        left_mm, right_mm = thresholds_mm
        if left_mm > right_mm:
            left_mm, right_mm = right_mm, left_mm  # ensure left <= right

        # Create figure/axes and step plot
        fig = plt.figure(num="Transect Graph")
        ax = fig.add_subplot(111)
        ax.step(x_mm, hu_values, where="mid")
        ax.set_xlabel("Distance (mm)")
        ax.set_ylabel("CT # ")
        ax.grid(True)

        # ROI overlay and data extraction
        roi_x_mm = np.array([])
        roi_hu = np.array([])
        if is_roi_draw:
            ax.axvline(left_mm, color="r")
            ax.axvline(right_mm, color="r")
            mask = (x_mm >= left_mm) & (x_mm <= right_mm)
            roi_x_mm = x_mm[mask]
            roi_hu = hu_values[mask]

        if show:
            plt.show(block = False)

        return {
            "x_mm": x_mm,
            "hu_values": hu_values,
            "thresholds_mm": (left_mm, right_mm),
            "roi_x_mm": roi_x_mm,
            "roi_hu": roi_hu,
            "figure": fig,
            "axes": ax,
        }
</code_context>

<issue_to_address>
**suggestion (code-quality):** Replace if statement with if expression ([`assign-if-exp`](https://docs.sourcery.ai/Reference/Default-Rules/refactorings/assign-if-exp/))

```suggestion
            thresholds_mm = (x_mm[0], x_mm[0]) if n == 1 else (x_mm[0], x_mm[-1])
```
</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.

Copy link
Collaborator

@Kahreiru Kahreiru left a comment

Choose a reason for hiding this comment

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

Looks good need to do something about those print statements still

Copy link
Collaborator

@sjswerdloff sjswerdloff left a comment

Choose a reason for hiding this comment

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

not sure what the motivation was for the changes, but it looks ok.

@didymo didymo merged commit d795767 into didymo:dev Oct 23, 2025
7 checks passed
@AJB-BigA
Copy link
Collaborator Author

not sure what the motivation was for the changes, but it looks ok.

The old transect tool was still a dummy version and scroll to change slides was added

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.

4 participants