Skip to content

Commit 6296668

Browse files
committed
CurveStatsTool and ImageStatsTool are now customizable
1 parent 0ee2cd5 commit 6296668

File tree

9 files changed

+349
-182
lines changed

9 files changed

+349
-182
lines changed

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,24 @@
11
# Changelog #
22

3+
## Version 2.4.0 ##
4+
5+
In this release, test coverage is 79%.
6+
7+
💥 New features / Enhancements:
8+
9+
* Curve statistics tool `CurveStatsTool` is now customizable:
10+
* When adding the tool: `plot_widget.manager.add_tool(CurveStatsTool, labelfuncs=(...))`
11+
* Or after: `plot_widget.manager.get_tool(CurveStatsTool).set_labelfuncs(...)`
12+
* The `labelfuncs` parameter is a list of tuples `(label, func)` where `label` is the
13+
label displayed in the statistics table, and `func` is a function that takes the
14+
curve data and returns the corresponding statistic value (see the documentation for
15+
more details)
16+
* Image statistics tool `ImageStatsTool` is now customizable:
17+
* When adding the tool: `plot_widget.manager.add_tool(ImageStatsTool, stats_func=...)`
18+
* Or after: `plot_widget.manager.get_tool(ImageStatsTool).set_stats_func(...)`
19+
* The `stats_func` parameter is a function that takes the image item and selected
20+
rectangle coordinates, and returns a string with the statistics to display
21+
322
## Version 2.3.5 ##
423

524
This release is mainly intended to fix the Windows binary distribution, which was not

doc/features/tools/overview.rst

Lines changed: 0 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -26,57 +26,3 @@ integrated manager, tools and panels.
2626

2727
:ref:`items`
2828
Plot items: curves, images, markers, etc.
29-
30-
31-
The `tools` module provides the following tools:
32-
33-
* :py:class:`.tools.RectZoomTool`
34-
* :py:class:`.tools.SelectTool`
35-
* :py:class:`.tools.SelectPointTool`
36-
* :py:class:`.tools.RotationCenterTool`
37-
* :py:class:`.tools.MultiLineTool`
38-
* :py:class:`.tools.FreeFormTool`
39-
* :py:class:`.tools.LabelTool`
40-
* :py:class:`.tools.RectangularShapeTool`
41-
* :py:class:`.tools.RectangleTool`
42-
* :py:class:`.tools.PointTool`
43-
* :py:class:`.tools.SegmentTool`
44-
* :py:class:`.tools.CircleTool`
45-
* :py:class:`.tools.EllipseTool`
46-
* :py:class:`.tools.PlaceAxesTool`
47-
* :py:class:`.tools.AnnotatedRectangleTool`
48-
* :py:class:`.tools.AnnotatedCircleTool`
49-
* :py:class:`.tools.AnnotatedEllipseTool`
50-
* :py:class:`.tools.AnnotatedPointTool`
51-
* :py:class:`.tools.AnnotatedSegmentTool`
52-
* :py:class:`.tools.HRangeTool`
53-
* :py:class:`.tools.DummySeparatorTool`
54-
* :py:class:`.tools.AntiAliasingTool`
55-
* :py:class:`.tools.DownSamplingTool`
56-
* :py:class:`.tools.DisplayCoordsTool`
57-
* :py:class:`.tools.ReverseYAxisTool`
58-
* :py:class:`.tools.AspectRatioTool`
59-
* :py:class:`.tools.PanelTool`
60-
* :py:class:`.tools.ItemListPanelTool`
61-
* :py:class:`.tools.ContrastPanelTool`
62-
* :py:class:`.tools.ColormapTool`
63-
* :py:class:`.tools.ReverseColormapTool`
64-
* :py:class:`.tools.XCSPanelTool`
65-
* :py:class:`.tools.YCSPanelTool`
66-
* :py:class:`.tools.CrossSectionTool`
67-
* :py:class:`.tools.AverageCrossSectionTool`
68-
* :py:class:`.tools.LineCrossSectionTool`
69-
* :py:class:`.tools.SaveAsTool`
70-
* :py:class:`.tools.CopyToClipboardTool`
71-
* :py:class:`.tools.OpenFileTool`
72-
* :py:class:`.tools.OpenImageTool`
73-
* :py:class:`.tools.SnapshotTool`
74-
* :py:class:`.tools.PrintTool`
75-
* :py:class:`.tools.SaveItemsTool`
76-
* :py:class:`.tools.LoadItemsTool`
77-
* :py:class:`.tools.AxisScaleTool`
78-
* :py:class:`.tools.HelpTool`
79-
* :py:class:`.tools.ExportItemDataTool`
80-
* :py:class:`.tools.EditItemDataTool`
81-
* :py:class:`.tools.ItemCenterTool`
82-
* :py:class:`.tools.DeleteItemTool`

doc/features/tools/reference.rst

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,6 @@ Base classes
1919
.. autoclass:: plotpy.tools.PanelTool
2020
:members:
2121

22-
Selection tools
23-
^^^^^^^^^^^^^^^
24-
25-
.. autoclass:: plotpy.tools.SelectTool
26-
:members:
27-
.. autoclass:: plotpy.tools.SelectPointTool
28-
:members:
29-
3022
Plot tools
3123
^^^^^^^^^^
3224

@@ -44,6 +36,8 @@ Plot tools
4436
Item tools
4537
^^^^^^^^^^
4638

39+
.. autoclass:: plotpy.tools.SelectTool
40+
:members:
4741
.. autoclass:: plotpy.tools.ItemListPanelTool
4842
:members:
4943
.. autoclass:: plotpy.tools.SaveItemsTool
@@ -93,9 +87,23 @@ Shape tools
9387
.. autoclass:: plotpy.tools.HRangeTool
9488
:members:
9589

90+
Curve tools
91+
^^^^^^^^^^^
92+
93+
.. autoclass:: plotpy.tools.CurveStatsTool
94+
:members:
95+
.. autoclass:: plotpy.tools.SelectPointTool
96+
:members:
97+
.. autoclass:: plotpy.tools.SelectPointsTool
98+
:members:
99+
.. autoclass:: plotpy.tools.EditPointTool
100+
:members:
101+
96102
Image tools
97103
^^^^^^^^^^^
98104

105+
.. autoclass:: plotpy.tools.ImageStatsTool
106+
:members:
99107
.. autoclass:: plotpy.tools.RotationCenterTool
100108
:members:
101109
.. autoclass:: plotpy.tools.ReverseYAxisTool

doc/requirements.rst

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ The :mod:`plotpy` package requires the following Python modules:
99
- Summary
1010
* - Python
1111
- >=3.8, <4
12-
-
12+
- Python programming language
1313
* - guidata
1414
- >=3.4
1515
- Automatic GUI generation for easy dataset editing and display
@@ -41,12 +41,9 @@ Optional modules for development:
4141
* - Name
4242
- Version
4343
- Summary
44-
* - black
45-
-
46-
- The uncompromising code formatter.
47-
* - isort
44+
* - ruff
4845
-
49-
- A Python utility / library to sort Python imports.
46+
- An extremely fast Python linter and code formatter, written in Rust.
5047
* - pylint
5148
-
5249
- python code static checker

plotpy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
.. _GitHub: https://github.com/PierreRaybaut/plotpy
2121
"""
2222

23-
__version__ = "2.3.5"
23+
__version__ = "2.4.0"
2424
__VERSION__ = tuple([int(number) for number in __version__.split(".")])
2525

2626
# --- Important note: DATAPATH and LOCALEPATH are used by guidata.configtools

plotpy/styles/image.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,26 @@ class BaseImageParam(DataSet):
106106
zformat = StringItem(_("Z-Axis"), default=r"%.1f")
107107
_end_formats = EndGroup(_("Statistics string formatting"))
108108

109+
def get_units(self) -> tuple[str, str, str]:
110+
"""Get the units for the x, y and z axes.
111+
112+
Returns:
113+
The units for the x, y and z axes.
114+
"""
115+
try:
116+
xunit = self.xformat.split()[1]
117+
except IndexError:
118+
xunit = ""
119+
try:
120+
yunit = self.yformat.split()[1]
121+
except IndexError:
122+
yunit = ""
123+
try:
124+
zunit = self.zformat.split()[1]
125+
except IndexError:
126+
zunit = ""
127+
return xunit, yunit, zunit
128+
109129
def update_param(self, image: BaseImageItem) -> None:
110130
"""Update the parameters from the given image item.
111131
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
# -*- coding: utf-8 -*-
2+
#
3+
# Licensed under the terms of the BSD 3-Clause
4+
# (see plotpy/LICENSE for details)
5+
6+
"""Curve and Image Statistics tools tests"""
7+
8+
# guitest: show
9+
10+
from __future__ import annotations
11+
12+
from typing import TYPE_CHECKING
13+
14+
import numpy as np
15+
from guidata.qthelpers import create_toolbutton, execenv, qt_app_context
16+
17+
from plotpy.builder import make
18+
from plotpy.config import _
19+
from plotpy.constants import PlotType
20+
from plotpy.plot import PlotDialog, PlotOptions
21+
from plotpy.tests.data import gen_image4
22+
from plotpy.tests.unit.utils import drag_mouse
23+
from plotpy.tools import CurveStatsTool, ImageStatsTool
24+
25+
if TYPE_CHECKING:
26+
from plotpy.items.image.base import BaseImageItem
27+
from plotpy.styles import BaseImageParam
28+
29+
30+
class MyPlotDialog(PlotDialog):
31+
def __init__(self, type: PlotType) -> None:
32+
"""Reimplement PlotDialog method"""
33+
super().__init__(
34+
title=f"{type.name.lower().capitalize()} Statistics tools test",
35+
toolbar=True,
36+
options=PlotOptions(type=type),
37+
)
38+
# No need to add the tools to the manager, they are automatically added
39+
# when the `register_curve_tools` or `register_image_tools` method is called
40+
self.setup_items()
41+
if execenv.unattended:
42+
self.simulate_stats_tool()
43+
44+
def populate_plot_layout(self) -> None:
45+
"""Populate the plot layout"""
46+
super().populate_plot_layout()
47+
options = self.plot_widget.options
48+
btn = create_toolbutton(
49+
self,
50+
None,
51+
f"Simulate {options.type.name.lower()} statistics tool",
52+
self.simulate_stats_tool,
53+
)
54+
self.add_widget(btn)
55+
56+
def simulate_stats_tool(self) -> None:
57+
"""Simulate the statistics tool"""
58+
options = self.plot_widget.options
59+
klass = CurveStatsTool if options.type is PlotType.CURVE else ImageStatsTool
60+
tool = self.manager.get_tool(klass)
61+
tool.activate()
62+
drag_mouse(self, np.array([0.4, 0.8]), np.array([0.2, 0.7]))
63+
64+
def setup_items(self) -> None:
65+
"""Setup items"""
66+
plot = self.get_plot()
67+
if self.plot_widget.options.type is PlotType.CURVE:
68+
x = np.linspace(0, 10, 100)
69+
y = np.sin(x)
70+
item = make.curve(x, y, title="Curve")
71+
else:
72+
item = make.image(gen_image4(500, 500), title="Image", colormap="cool")
73+
plot.add_item(item)
74+
plot.select_item(item)
75+
76+
77+
def test_curve_stats_tools() -> None:
78+
"""Test"""
79+
with qt_app_context(exec_loop=True):
80+
win = MyPlotDialog(type=PlotType.CURVE)
81+
tool = win.manager.get_tool(CurveStatsTool)
82+
labelfuncs = (
83+
("Mean = %g", lambda *args: np.mean(args[1])),
84+
("Std = %g", lambda *args: np.std(args[1])),
85+
("Max = %g", lambda *args: np.max(args[1])),
86+
("Min = %g", lambda *args: np.min(args[1])),
87+
)
88+
tool.set_labelfuncs(labelfuncs)
89+
win.show()
90+
91+
92+
# Note:
93+
# -----
94+
#
95+
# Using the following `get_more_stats` function, the `ImageStatsTool` will display
96+
# the surface, the sum and the density of the selected area - as additional stats
97+
# if the `replace` parameter is set to `False` (default value).
98+
#
99+
# This is the way of reimplementing the old `show_surface` and `show_integral`
100+
# arguments of the `ImageStatsTool` class in previous versions of PlotPy.
101+
102+
103+
def get_more_stats(
104+
item: BaseImageItem,
105+
x0: float,
106+
y0: float,
107+
x1: float,
108+
y1: float,
109+
) -> str:
110+
"""Return formatted string with stats on image rectangular area
111+
(output should be compatible with AnnotatedShape.get_infos)
112+
113+
Args:
114+
item: image item
115+
x0: X0
116+
y0: Y0
117+
x1: X1
118+
y1: Y1
119+
"""
120+
ix0, iy0, ix1, iy1 = item.get_closest_index_rect(x0, y0, x1, y1)
121+
data = item.data[iy0:iy1, ix0:ix1]
122+
p: BaseImageParam = item.param
123+
xunit, yunit, zunit = p.get_units()
124+
infos = ""
125+
if xunit == yunit:
126+
surfacefmt = p.xformat.split()[0] + " " + xunit
127+
if xunit != "":
128+
surfacefmt = surfacefmt + "²"
129+
surface = abs((x1 - x0) * (y1 - y0))
130+
infos += _("surface = %s") % (surfacefmt % surface)
131+
integral = data.sum()
132+
integral_fmt = r"%.3e " + zunit
133+
infos = infos + "<br>" + _("sum = %s") % (integral_fmt % integral)
134+
if xunit == yunit and xunit is not None and zunit is not None:
135+
if surface != 0:
136+
density = integral / surface
137+
densityfmt = r"%.3e"
138+
if xunit and zunit:
139+
densityfmt += " " + zunit + "/" + xunit + "²"
140+
infos = infos + "<br>" + _("density = %s") % (densityfmt % density)
141+
else:
142+
infos = infos + "<br>" + _("density not computed : surface is null !")
143+
return infos
144+
145+
146+
def test_image_stats_tools() -> None:
147+
"""Test"""
148+
with qt_app_context(exec_loop=True):
149+
win = MyPlotDialog(type=PlotType.IMAGE)
150+
tool = win.manager.get_tool(ImageStatsTool)
151+
tool.set_stats_func(get_more_stats, replace=False)
152+
win.show()
153+
154+
155+
if __name__ == "__main__":
156+
test_curve_stats_tools()
157+
test_image_stats_tools()

0 commit comments

Comments
 (0)