Skip to content

Commit 81dd53d

Browse files
committed
fix(csection): compute cross sections of images added or shown again
When an image was added to the plot, or shown again after having been hidden, while a cross-section marker or shape was active, its X/Y cross-section curves stayed empty until the marker was moved: items_changed created the curves after updating the plot, and never computed them. - Create the curves of added or shown again images before updating the plot, then compute these new curves only (unless the auto-refresh mode is disabled), so that the other curves are not computed again - Compute the shade of the curves from the number of visible images, instead of all images (hidden ones included) - Flush the closing of the previous test windows in the tests, which would otherwise hide the cross-section panels Refs #89 Assisted-by: Claude Opus 5.5
1 parent 9984b49 commit 81dd53d

2 files changed

Lines changed: 132 additions & 10 deletions

File tree

‎plotpy/panels/csection/csplot.py‎

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -173,34 +173,50 @@ def items_changed(self, plot: BasePlot) -> None:
173173
make every change cost more as images accumulate in the plot (e.g. an
174174
application showing one image at a time among many hidden ones).
175175
176+
The curves of image items which are added or shown again are computed
177+
right away from the last marker or shape used for the cross section,
178+
unless the auto-refresh mode is disabled.
179+
176180
Args:
177181
plot: Plot containing the image items to be used for cross section
178182
"""
179183
new_sources = plot.get_items(item_type=ICSImageItemType)
184+
visible_sources = [source for source in new_sources if source.isVisible()]
180185
# Del cross section items of removed or hidden image items
181186
for source in self.known_items.copy():
182-
if source not in new_sources or not source.isVisible():
187+
if source not in visible_sources:
183188
curve = self.known_items.pop(source)
184189
curve.clear_data() # useful to emit SIG_CS_CURVE_CHANGED
185190
# (eventually notify other panels that the
186191
# cross section curve is now empty)
187192
self.del_item(curve)
188193

194+
# Add cross section items of added or shown again image items
195+
# (the shade is applied to the curves when they are created)
196+
if visible_sources:
197+
self.param.shade = self.SHADE / len(visible_sources)
198+
new_curves: list[CrossSectionItem] = []
199+
for source in visible_sources:
200+
if source not in self.known_items:
201+
if not self.single_source or not self.known_items:
202+
self.add_cross_section_item(source=source)
203+
new_curves.append(self.known_items[source])
204+
189205
# Update plot only to show/hide cross section curves according to
190206
# the associated image item visibility state (hence `refresh=False`)
191207
self.update_plot(refresh=False)
192208

209+
# Compute the new curves only: the other curves are already up to date
210+
obj = self.get_last_obj()
211+
if new_curves and self.autorefresh_mode and obj is not None:
212+
if obj.plot() is not None:
213+
for curve in new_curves:
214+
curve.update_item(obj)
215+
193216
self.plot_axis_changed(plot)
194217

195218
if not new_sources:
196219
self.replot()
197-
return
198-
199-
self.param.shade = self.SHADE / len(new_sources)
200-
for source in new_sources:
201-
if source not in self.known_items and source.isVisible():
202-
if not self.single_source or not self.known_items:
203-
self.add_cross_section_item(source=source)
204220

205221
def active_item_changed(self, plot: BasePlot) -> None:
206222
"""Active item has just changed

‎plotpy/tests/unit/test_cross_section_visible_items.py‎

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,55 @@
33
# Licensed under the terms of the BSD 3-Clause
44
# (see plotpy/LICENSE for details)
55

6-
"""Cross section panels keep curves for visible images only; unchanged axis
7-
styles do not lay out the plot again."""
6+
"""Cross section panels keep curves for visible images only, and compute the
7+
curves of images added or shown again; unchanged axis styles do not lay out the
8+
plot again."""
89

910
from __future__ import annotations
1011

1112
import numpy as np
13+
import pytest
1214
from guidata.qthelpers import qt_app_context
1315
from qtpy import QtWidgets as QW
1416

1517
from plotpy.builder import make
1618

1719

1820
def _window():
21+
# In unattended mode, leaving a `qt_app_context` schedules the closing of all
22+
# top-level widgets: process it now, so that it does not close this window
23+
QW.QApplication.processEvents()
1924
win = make.dialog(type="image", show_xsection=True, show_ysection=True)
2025
win.show()
2126
return win
2227

2328

29+
def _cs_plots(win):
30+
"""Return the X and Y cross section plots of the window"""
31+
return win.manager.get_xcs_panel().cs_plot, win.manager.get_ycs_panel().cs_plot
32+
33+
34+
def _add_marker(win, x: float, y: float):
35+
"""Add a marker to the plot and register it for the cross sections"""
36+
plot = win.manager.get_plot()
37+
marker = make.marker(position=(x, y))
38+
plot.add_item(marker)
39+
for cs_plot in _cs_plots(win):
40+
cs_plot.register_shape(plot, marker)
41+
return marker
42+
43+
44+
def _assert_sections(win, image, x: float, y: float) -> None:
45+
"""Assert that the cross section curves of the image are the sections at
46+
(x, y)"""
47+
xcs, ycs = _cs_plots(win)
48+
xcurve, ycurve = xcs.known_items[image], ycs.known_items[image]
49+
assert xcurve.dataSize() > 0 and ycurve.dataSize() > 0
50+
# The Y cross section curve is vertical: section values are its X data
51+
assert np.array_equal(xcurve.get_data()[1], image.get_xsection(y)[1])
52+
assert np.array_equal(ycurve.get_data()[0], image.get_ysection(x)[1])
53+
54+
2455
def test_cross_section_curves_follow_visible_images():
2556
"""Hidden images have no cross section curve, until they are shown again."""
2657
with qt_app_context(exec_loop=False):
@@ -49,6 +80,77 @@ def test_cross_section_curves_follow_visible_images():
4980
win.close()
5081

5182

83+
def test_cross_section_computed_when_image_shown_again():
84+
"""The cross sections of an image shown again are those at the marker
85+
position, even if the marker was moved while the image was hidden."""
86+
with qt_app_context(exec_loop=False):
87+
win = _window()
88+
plot = win.manager.get_plot()
89+
image1 = make.image(np.arange(16 * 16, dtype=float).reshape(16, 16))
90+
image2 = make.image(np.full((16, 16), 42.0))
91+
plot.add_item(image1)
92+
plot.add_item(image2)
93+
marker = _add_marker(win, 5, 5)
94+
_assert_sections(win, image1, 5, 5)
95+
plot.set_item_visible(image1, False)
96+
marker.set_pos(10, 12)
97+
plot.set_item_visible(image1, True)
98+
_assert_sections(win, image1, 10, 12)
99+
_assert_sections(win, image2, 10, 12)
100+
win.close()
101+
102+
103+
def test_cross_section_computed_for_added_image():
104+
"""The cross sections of an image added to the plot are computed at once."""
105+
with qt_app_context(exec_loop=False):
106+
win = _window()
107+
plot = win.manager.get_plot()
108+
plot.add_item(make.image(np.full((16, 16), 42.0)))
109+
_add_marker(win, 5, 7)
110+
image = make.image(np.arange(16 * 16, dtype=float).reshape(16, 16))
111+
plot.add_item(image)
112+
_assert_sections(win, image, 5, 7)
113+
win.close()
114+
115+
116+
def test_cross_section_of_shown_image_follows_autorefresh_mode():
117+
"""With the auto-refresh mode disabled, the cross section curves of an image
118+
shown again are computed on the next refresh only."""
119+
with qt_app_context(exec_loop=False):
120+
win = _window()
121+
plot = win.manager.get_plot()
122+
xcs, _ycs = _cs_plots(win)
123+
image = make.image(np.arange(16 * 16, dtype=float).reshape(16, 16))
124+
plot.add_item(image)
125+
_add_marker(win, 5, 5)
126+
xcs.toggle_autorefresh(False)
127+
plot.set_item_visible(image, False)
128+
plot.set_item_visible(image, True)
129+
assert xcs.known_items[image].dataSize() == 0
130+
xcs.update_plot()
131+
assert xcs.known_items[image].dataSize() == 16
132+
win.close()
133+
134+
135+
def test_cross_section_shade_counts_visible_images_only():
136+
"""The shade of the cross section curves ignores hidden images."""
137+
with qt_app_context(exec_loop=False):
138+
win = _window()
139+
plot = win.manager.get_plot()
140+
xcs, _ycs = _cs_plots(win)
141+
images = []
142+
for _index in range(4):
143+
if images:
144+
plot.set_item_visible(images[-1], False)
145+
image = make.image(np.ones((16, 16)))
146+
plot.add_item(image)
147+
images.append(image)
148+
assert xcs.param.shade == pytest.approx(xcs.SHADE)
149+
brush_color = xcs.known_items[images[-1]].brush().color()
150+
assert brush_color.alphaF() == pytest.approx(xcs.SHADE, abs=1e-2)
151+
win.close()
152+
153+
52154
def test_unchanged_axis_style_does_not_relayout():
53155
"""Setting the same titles again neither lays out the plot nor notifies."""
54156
with qt_app_context(exec_loop=False):
@@ -70,4 +172,8 @@ def test_unchanged_axis_style_does_not_relayout():
70172

71173
if __name__ == "__main__":
72174
test_cross_section_curves_follow_visible_images()
175+
test_cross_section_computed_when_image_shown_again()
176+
test_cross_section_computed_for_added_image()
177+
test_cross_section_of_shown_image_follows_autorefresh_mode()
178+
test_cross_section_shade_counts_visible_images_only()
73179
test_unchanged_axis_style_does_not_relayout()

0 commit comments

Comments
 (0)