3434
3535if TYPE_CHECKING :
3636 import guidata .io
37- import qwt .color_map
3837 import qwt .scale_map
3938 from qtpy .QtCore import QPointF , QRectF
4039 from qtpy .QtGui import QColor , QPainter
@@ -510,10 +509,17 @@ class XYImageItem(RawImageItem):
510509 """XY image item (non-linear axes)
511510
512511 Args:
513- x: 1D NumPy array, must be increasing
514- y: 1D NumPy array, must be increasing
512+ x: 1D NumPy array of pixel center coordinates , must be increasing
513+ y: 1D NumPy array of pixel center coordinates , must be increasing
515514 data: 2D NumPy array
516515 param: image parameters
516+
517+ Note:
518+ Internally, `self.x` and `self.y` store **bin edges** (boundaries between
519+ pixels), not pixel centers. If input arrays have length nj and ni (matching
520+ data dimensions), they are converted to bin edges of length nj+1 and ni+1
521+ using `to_bins()`. Methods that need pixel center coordinates must compute
522+ them from the stored bin edges.
517523 """
518524
519525 __implements__ = (IBasePlotItem , IBaseImageItem , ISerializableType )
@@ -604,12 +610,19 @@ def set_xy(self, x: np.ndarray | list[float], y: np.ndarray | list[float]) -> No
604610 """Set X and Y data
605611
606612 Args:
607- x: 1D NumPy array, must be increasing
608- y: 1D NumPy array, must be increasing
613+ x: 1D NumPy array of pixel center coordinates , must be increasing
614+ y: 1D NumPy array of pixel center coordinates , must be increasing
609615
610616 Raises:
611617 ValueError: If X or Y are not increasing
612618 IndexError: If X or Y are not of the right length
619+
620+ Note:
621+ Input arrays represent pixel **center** coordinates. Internally, they are
622+ stored as bin **edges** in `self.x` and `self.y`:
623+ - If input has length nj (or ni), it's converted to edges via `to_bins()`
624+ - If input already has length nj+1 (or ni+1), it's used directly as edges
625+ - The stored edges have length nj+1 and ni+1 (one more than data dimensions)
613626 """
614627 ni , nj = self .data .shape
615628 x = np .array (x , float )
@@ -677,9 +690,18 @@ def get_pixel_coordinates(self, xplot: float, yplot: float) -> tuple[float, floa
677690 yplot: Y plot coordinate
678691
679692 Returns:
680- Pixel coordinates
681- """
682- return self .x .searchsorted (xplot ), self .y .searchsorted (yplot )
693+ Pixel coordinates (integer pixel indices)
694+ """
695+ # self.x and self.y are bin edges. searchsorted finds the right edge index.
696+ # To get the pixel index, we need the left edge index, which is right_edge - 1
697+ # But clamp to valid range [0, n-1] where n is number of pixels
698+ i = self .x .searchsorted (xplot )
699+ j = self .y .searchsorted (yplot )
700+ # searchsorted returns the insertion point (right edge index)
701+ # Pixel index is insertion_point - 1, but clamped to [0, data.shape-1]
702+ i = max (0 , min (i - 1 , self .data .shape [1 ] - 1 ))
703+ j = max (0 , min (j - 1 , self .data .shape [0 ] - 1 ))
704+ return i , j
683705
684706 def get_plot_coordinates (self , xpixel : float , ypixel : float ) -> tuple [float , float ]:
685707 """Get plot coordinates from pixel coordinates
@@ -689,9 +711,17 @@ def get_plot_coordinates(self, xpixel: float, ypixel: float) -> tuple[float, flo
689711 ypixel: Y pixel coordinate
690712
691713 Returns:
692- Plot coordinates
693- """
694- return self .x [int (pixelround (xpixel ))], self .y [int (pixelround (ypixel ))]
714+ Plot coordinates (pixel center coordinates)
715+ """
716+ # self.x and self.y store bin edges, compute centers
717+ i = int (pixelround (xpixel ))
718+ j = int (pixelround (ypixel ))
719+ # Protect against out-of-bounds access
720+ i = max (0 , min (i , len (self .x ) - 2 ))
721+ j = max (0 , min (j , len (self .y ) - 2 ))
722+ x_center = (self .x [i ] + self .x [i + 1 ]) / 2.0
723+ y_center = (self .y [j ] + self .y [j + 1 ]) / 2.0
724+ return x_center , y_center
695725
696726 def get_x_values (self , i0 : int , i1 : int ) -> np .ndarray :
697727 """Get X values from pixel indexes
@@ -701,12 +731,13 @@ def get_x_values(self, i0: int, i1: int) -> np.ndarray:
701731 i1: Second index
702732
703733 Returns:
704- X values corresponding to the given pixel indexes
734+ X values corresponding to the given pixel indexes (pixel centers)
705735 """
706- # Returning a copy to prevent modification of internal data by caller
736+ # self.x stores bin edges (length nj+1), but we need to return pixel centers
737+ # Compute centers from edges: center[i] = (edge[i] + edge[i+1]) / 2
707738 # (Fixes issue #49 - Using cross-section tools on `XYImageItem` images alters
708739 # the X/Y coordinate arrays)
709- return self .x [i0 :i1 ]. copy ()
740+ return ( self .x [i0 :i1 ] + self . x [ i0 + 1 : i1 + 1 ]) / 2.0
710741
711742 def get_y_values (self , j0 : int , j1 : int ) -> np .ndarray :
712743 """Get Y values from pixel indexes
@@ -716,9 +747,11 @@ def get_y_values(self, j0: int, j1: int) -> np.ndarray:
716747 j1: Second index
717748
718749 Returns:
719- Y values corresponding to the given pixel indexes
750+ Y values corresponding to the given pixel indexes (pixel centers)
720751 """
721- return self .y [j0 :j1 ].copy () # (same remark as in `get_x_values`)
752+ # self.y stores bin edges (length ni+1), but we need to return pixel centers
753+ # Compute centers from edges: center[j] = (edge[j] + edge[j+1]) / 2
754+ return (self .y [j0 :j1 ] + self .y [j0 + 1 : j1 + 1 ]) / 2.0
722755
723756 def get_closest_coordinates (self , x : float , y : float ) -> tuple [float , float ]:
724757 """
@@ -729,10 +762,16 @@ def get_closest_coordinates(self, x: float, y: float) -> tuple[float, float]:
729762 y: Y coordinate
730763
731764 Returns:
732- tuple[float, float]: Closest coordinates
765+ tuple[float, float]: Closest pixel center coordinates
733766 """
734767 i , j = self .get_closest_indexes (x , y )
735- return self .x [i ], self .y [j ]
768+ # self.x and self.y store bin edges, compute centers
769+ # Protect against out-of-bounds access
770+ i = max (0 , min (i , len (self .x ) - 2 ))
771+ j = max (0 , min (j , len (self .y ) - 2 ))
772+ x_center = (self .x [i ] + self .x [i + 1 ]) / 2.0
773+ y_center = (self .y [j ] + self .y [j + 1 ]) / 2.0
774+ return x_center , y_center
736775
737776 # ---- IBasePlotItem API ---------------------------------------------------
738777 def types (self ) -> tuple [type [IItemType ], ...]:
0 commit comments