Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
42915bf
Fix: Sort Y values and use abs(delta) in YRangeCursorTool LABELFUNCS
dappham-CODRA Apr 17, 2026
2d9124e
bump version to 2.9.1
PierreRaybaut Apr 22, 2026
7b38c01
fix: rectangular snapshot "Original size" on reversed axes and XYImag…
PierreRaybaut Apr 22, 2026
dc4c23e
feat: add edge width parameter to SymbolParam for customizable border…
PierreRaybaut Apr 22, 2026
1a49db0
fix: ensure consistent original size reporting for selections larger …
PierreRaybaut Apr 23, 2026
b25614c
Fix snapshot tool's "Original size" to preserve native pixel resolution
PierreRaybaut Apr 24, 2026
4a3eeb3
Fix snapshot tool's stuck cross cursor outside the plot canvas
PierreRaybaut Apr 24, 2026
53af760
Fix Z-axis log tool being always disabled for non-ImageItem image types
PierreRaybaut Apr 24, 2026
6b6c139
Merge branch 'release' into develop
ThomasMalletCodra May 7, 2026
a1f985c
feat(plot): add per-axis autoscale strategy (auto / fixed / none)
PierreRaybaut May 12, 2026
e1779ac
fix: update log data when z-axis log scale is enabled
ovallcorba May 14, 2026
7013dad
refactor(image): factor out _recompute_log_data and honor keep_lut_ra…
PierreRaybaut May 14, 2026
3ddb839
Merge pull request #62 from ovallcorba/fix/update_log_data_in_set_data
PierreRaybaut May 14, 2026
38d0e27
Fix plot nan values
dappham-CODRA May 18, 2026
6fb84f4
fix linter warnings
ThomasMalletCodra May 22, 2026
51bca46
update translation
ThomasMalletCodra May 22, 2026
5f614f7
Merge pull request #65 from PlotPyStack/fix/issues-55-deltacursory
ThomasMalletCodra May 22, 2026
cd6b28f
use new PythonQwt 0.16 version to get the performance fix apply
ThomasMalletCodra May 7, 2026
d819f8c
use new PythonQwt 0.16 version to get the performance fix apply
ThomasMalletCodra May 7, 2026
ecbf089
Merge branch 'release' into develop
ThomasMalletCodra Jun 2, 2026
0a641c2
update changelog for version 2.10.0
ThomasMalletCodra Jun 2, 2026
0ec5de3
update translation and generated requirements
ThomasMalletCodra Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions doc/release_notes/release_2.10.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Version 2.10 #

## PlotPy Version 2.10.0 ##

✨ New features:

* **Per-axis autoscale strategy**: Added configurable autoscale behavior for each axis via the axis parameters dialog. Three strategies are available: *Auto* (default — compute bounds from items), *Fixed range* (apply user-defined Min/Max values) and *Disabled* (leave the axis untouched on autoscale). New API: `BasePlot.set_axis_autoscale_strategy()` / `BasePlot.get_axis_autoscale_strategy()` (closes [Issue #63](https://github.com/PlotPyStack/PlotPy/issues/63), partial)
* **Symbol border width**: Added an `edgewidth` parameter to `SymbolParam` for customizable marker border thickness — previously the border was always 1 pixel wide

🛠️ Bug fixes:

* **Rectangular snapshot tool** — Fixed the "Original size" computation (closes [Issue #57](https://github.com/PlotPyStack/PlotPy/issues/57)):
* The preview no longer displays negative dimensions when the X or Y axis is
reversed
* The "Original size" is now computed from pixel coordinates instead of axis
units, so it is correct for `XYImageItem` (and any item with non-uniform
axis scaling) regardless of axis orientation
* The `ValueError` raised by the resize dialog when the selection produced
negative dimensions on a reversed axis is gone
* Selecting a region larger than the plotted image now reports the same
native pixel resolution for both `ImageItem` and `XYImageItem`
(previously `XYImageItem` reported ``shape - 1`` while `ImageItem`
reported the full oversized resolution): exporting at "Original size"
now consistently preserves the source pixel density and avoids
upsampling, regardless of the item type
* **Snapshot tool cursor** — Fixed the mouse cursor remaining stuck as a cross (`+`) outside the plot canvas (axes, toolbar) after using the snapshot tool. The modal dialogs are now opened after Qt has released the implicit pointer grab, so the cursor is correctly restored (closes [Issue #58](https://github.com/PlotPyStack/PlotPy/issues/58))
* **Z-axis log tool** — Fixed the `ZAxisLogTool` being always disabled for non-`ImageItem` image types (`XYImageItem`, `MaskedImageItem`, `MaskedXYImageItem`, `TrImageItem`, `RGBImageItem`). The Z-axis log API (`get_zaxis_log_state` / `set_zaxis_log_state`) was moved from `ImageItem` up to `BaseImageItem` so all image item types support it. This notably fixes the tool being permanently greyed out in DataLab's image panel (closes [Issue #59](https://github.com/PlotPyStack/PlotPy/issues/59))
* **Z-axis log data update** — Fixed image data not being recomputed when calling `set_data()` while Z-axis log scale is active — the log-transformed data is now refreshed and the LUT range preserved in log mode
* **`YRangeCursorTool`** — Fixed incorrect inequality display and negative ∆y when the Y-range cursors are inverted (dragging the top cursor below the bottom one). Values are now sorted and ∆y is always positive (closes [Issue #55](https://github.com/PlotPyStack/PlotPy/issues/55))
* **`CurveStatsTool`** — Replaced `min`/`max`/`mean`/`std`/`sum` with their NaN-safe equivalents (`nanmin`, `nanmax`, `nanmean`, `nanstd`, `nansum`) so that signal statistics are computed correctly when the data contains NaN values

⚙️ Dependencies:

* Bumped minimum PythonQwt version from 0.15 to **0.16** to benefit from the Qt6 performance optimizations (closes [Issue #22](https://github.com/PlotPyStack/PlotPy/issues/22) — see [PythonQwt#93](https://github.com/PlotPyStack/PythonQwt/issues/93) for the full optimization log)
38 changes: 22 additions & 16 deletions doc/requirements.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The `PlotPy` package requires the following Python modules:
- >= 3.14.1
- Automatic GUI generation for easy dataset editing and display
* - PythonQwt
- >= 0.15
- >= 0.16
- Qt plotting widgets for Python
* - numpy
- >= 1.22
Expand All @@ -26,10 +26,10 @@ The `PlotPy` package requires the following Python modules:
- >= 0.19
- Image processing in Python
* - Pillow
-
-
- Python Imaging Library (fork)
* - tifffile
-
-
- Read and write TIFF files

Optional modules for GUI support (Qt):
Expand All @@ -55,26 +55,32 @@ Optional modules for development:
- Version
- Summary
* - build
-
-
- A simple, correct Python build frontend
* - babel
-
-
- Internationalization utilities
* - Coverage
-
-
- Code coverage measurement for Python
* - Cython
- >=3.0
- The Cython compiler for writing C extensions in the Python language.
* - pylint
-
-
- python code static checker
* - ruff
-
-
- An extremely fast Python linter and code formatter, written in Rust.
* - pre-commit
-
-
- A framework for managing and maintaining multi-language pre-commit hooks.
* - setuptools
-
- Most extensible Python build backend with support for C/C++ extension modules
* - wheel
-
- Command line tool for manipulating wheel files

Optional modules for building the documentation:

Expand All @@ -86,19 +92,19 @@ Optional modules for building the documentation:
- Version
- Summary
* - sphinx
-
-
- Python documentation generator
* - myst_parser
-
-
- An extended [CommonMark](https://spec.commonmark.org/) compliant parser,
* - sphinx-copybutton
-
-
- Add a copy button to each of your code cells.
* - sphinx_qt_documentation
-
-
- Plugin for proper resolve intersphinx references for Qt elements
* - python-docs-theme
-
-
- The Sphinx theme for the CPython docs and related projects

Optional modules for running test suite:
Expand All @@ -111,8 +117,8 @@ Optional modules for running test suite:
- Version
- Summary
* - pytest
-
-
- pytest: simple powerful testing with Python
* - pytest-xvfb
-
-
- A pytest plugin to run Xvfb (or Xephyr/Xvnc) for tests.
2 changes: 1 addition & 1 deletion plotpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
.. _GitHub: https://github.com/PierreRaybaut/plotpy
"""

__version__ = "2.9.0"
__version__ = "2.10.0"
__VERSION__ = tuple([int(number) for number in __version__.split(".")])

# --- Important note: DATAPATH and LOCALEPATH are used by guidata.configtools
Expand Down
1 change: 1 addition & 0 deletions plotpy/items/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
XYImageFilterItem,
XYImageItem,
assemble_imageitems,
compute_image_items_original_size,
compute_trimageitems_original_size,
get_image_from_plot,
get_image_from_qrect,
Expand Down
1 change: 1 addition & 0 deletions plotpy/items/image/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Histogram2DItem,
QuadGridItem,
assemble_imageitems,
compute_image_items_original_size,
compute_trimageitems_original_size,
get_image_from_plot,
get_image_from_qrect,
Expand Down
49 changes: 49 additions & 0 deletions plotpy/items/image/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ def __init__(
self._filename = None # The file this image comes from

self.histogram_cache = None

# Z-axis logarithmic scale support
self._log_data: np.ndarray | None = None
self._lin_lut_range: tuple[float, float] | None = None
self._is_zaxis_log = False

if data is not None:
self.set_data(data)
self.param.update_item(self)
Expand Down Expand Up @@ -334,6 +340,15 @@ def get_r_values(self, i0, i1, j0, j1, flag_circle=False):
"""
return self.get_x_values(i0, i1)

def _recompute_log_data(self) -> None:
"""Refresh the cached log10 data from the current ``self.data``.

Used both when toggling the Z-axis log scale on and when the underlying
data is replaced (e.g. via :meth:`set_data`) while the log scale is
already active.
"""
self._log_data = np.array(np.log10(self.data.clip(1)), dtype=np.float64)

def set_data(
self, data: np.ndarray, lut_range: tuple[float, float] | None = None
) -> None:
Expand All @@ -347,9 +362,15 @@ def set_data(
self.histogram_cache = None
self.update_bounds()
self.update_border()
# Refresh the cached log10 data when log scale is active, otherwise the
# display would keep using the previous (now stale) log data.
if self.get_zaxis_log_state():
self._recompute_log_data()
if not self.param.keep_lut_range:
if lut_range is not None:
_min, _max = lut_range
elif self.get_zaxis_log_state():
_min, _max = get_nan_range(self._log_data)
else:
_min, _max = get_nan_range(data)
self.set_lut_range((_min, _max))
Expand Down Expand Up @@ -552,6 +573,34 @@ def get_lut_range_full(self) -> tuple[float, float]:
"""
return get_nan_range(self.data)

# ---- Z-axis logarithmic scale --------------------------------------------
def get_zaxis_log_state(self) -> bool:
"""Return True if Z-axis is in logarithmic scale"""
return self._is_zaxis_log

def set_zaxis_log_state(self, state: bool) -> None:
"""Set Z-axis logarithmic scale state

Args:
state: True to enable logarithmic scale, False otherwise
"""
self._is_zaxis_log = state
plot = self.plot()
if state:
self._lin_lut_range = self.get_lut_range()
if self._log_data is None:
self._recompute_log_data()
self.set_lut_range(get_nan_range(self._log_data))
dtype = self._log_data.dtype
else:
self._log_data = None
self.set_lut_range(self._lin_lut_range)
dtype = self.data.dtype
if self.interpolate[0] == INTERP_AA:
self.interpolate = (INTERP_AA, self.interpolate[1].astype(dtype))
if plot is not None:
plot.update_colormap_axis(self)

def get_lut_range_max(self) -> tuple[float, float]:
"""Get maximum range for this dataset

Expand Down
95 changes: 89 additions & 6 deletions plotpy/items/image/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -577,19 +577,27 @@ def get_items_in_rectangle(

def compute_trimageitems_original_size(
items: list[TrImageItem],
src_w: list[float, float, float, float],
src_h: list[float, float, float, float],
src_w: float,
src_h: float,
) -> tuple[float, float]:
"""Compute `TrImageItem` original size from max dx and dy

Args:
items: List of image items
src_w: Source width
src_h: Source height
src_w: Source width (in plot axis units)
src_h: Source height (in plot axis units)

Returns:
Tuple of original size

.. note::

The returned size is always positive: when the source rectangle is
defined on a reversed axis, ``src_w`` and/or ``src_h`` may be
negative. The original (pixel) size is intrinsically positive,
independent of axis orientation.
"""
src_w, src_h = abs(src_w), abs(src_h)
trparams = [item.get_transform() for item in items if isinstance(item, TrImageItem)]
if trparams:
dx_max = max([dx for _x, _y, _angle, dx, _dy, _hf, _vf in trparams])
Expand All @@ -598,6 +606,81 @@ def compute_trimageitems_original_size(
return src_w, src_h


def compute_image_items_original_size(
items: list[BaseImageItem],
plot: qwt.plot.QwtPlot,
p0: QPointF,
p1: QPointF,
) -> tuple[float, float]:
"""Compute the **native pixel resolution** of a rectangular selection
across the given image items.

The "Original size" semantics is *original resolution*: the returned
size is the number of source pixels that span the selection at the
item's native resolution, *independent of axis orientation or scaling*.
When the selection is larger than the plotted image, the returned size
is consequently larger than the image (the missing area will be padded
by the export step). When the selection is smaller, it is smaller in
pixels — there is **no** clipping to the image bounding rectangle, so
that exporting at "Original size" always preserves the source pixel
density.

Args:
plot: Plot
items: List of image items in the selection
p0: First canvas point (top-left, in canvas coordinates)
p1: Second canvas point (bottom-right, in canvas coordinates)

Returns:
Tuple ``(width, height)`` in pixels (always positive). When no
compatible item is found, falls back to the absolute axis-units
size of the selection.
"""
p0x = plot.invTransform(X_BOTTOM, p0.x())
p0y = plot.invTransform(Y_LEFT, p0.y())
p1x = plot.invTransform(X_BOTTOM, p1.x() + 1)
p1y = plot.invTransform(Y_LEFT, p1.y() + 1)
sel_x0, sel_x1 = sorted([p0x, p1x])
sel_y0, sel_y1 = sorted([p0y, p1y])
sel_w = sel_x1 - sel_x0
sel_h = sel_y1 - sel_y0
widths: list[float] = []
heights: list[float] = []
for item in items:
data = getattr(item, "data", None)
if data is None:
continue
if isinstance(item, TrImageItem):
# Use the item's affine transform (handles rotation and shear)
get_pix = item.get_pixel_coordinates
try:
x0p, y0p = get_pix(sel_x0, sel_y0)
x1p, y1p = get_pix(sel_x1, sel_y1)
except (ValueError, TypeError, IndexError):
continue
widths.append(abs(x1p - x0p))
heights.append(abs(y1p - y0p))
else:
# For ImageItem / XYImageItem: convert the (possibly oversized)
# selection to pixels via the item's own pixel density. This
# avoids ``XYImageItem.get_pixel_coordinates`` clamping to
# integer indices and yields oversized values when the
# selection extends beyond the image — consistently with the
# historical behavior of ``ImageItem``.
brect = item.boundingRect()
bw = abs(brect.width())
bh = abs(brect.height())
if bw <= 0 or bh <= 0:
continue
widths.append(sel_w / bw * data.shape[1])
heights.append(sel_h / bh * data.shape[0])
if widths:
return max(widths), max(heights)
# Fallback: axis-units size (always positive)
_src_x, _src_y, src_w, src_h = get_plot_qrect(plot, p0, p1).getRect()
return abs(src_w), abs(src_h)


def get_image_from_qrect(
plot: BasePlot,
p0: QPointF,
Expand Down Expand Up @@ -636,12 +719,12 @@ def get_image_from_qrect(
if not items:
raise TypeError(_("There is no supported image item in current plot."))
if src_size is None:
_src_x, _src_y, src_w, src_h = get_plot_qrect(plot, p0, p1).getRect()
destw, desth = compute_image_items_original_size(items, plot, p0, p1)
else:
# The only benefit to pass the src_size list is to avoid any
# rounding error in the transformation computed in `get_plot_qrect`
src_w, src_h = src_size
destw, desth = compute_trimageitems_original_size(items, src_w, src_h)
destw, desth = compute_trimageitems_original_size(items, src_w, src_h)
data = get_image_from_plot(
plot,
p0,
Expand Down
Loading
Loading