Skip to content

Commit d740fb4

Browse files
Add only contour plot feature (#222)
* Add onlycontours option in dropdown * Update STATE_OPTIONS with onlycontours * Add support for 'onlycontours' division in plot count calculation * Add onlycontours option in dropdown * Update STATE_OPTIONS with onlycontours * Add support for 'onlycontours' division in plot count calculation * Add tests for 'onlycontours' division mode and zooming in on contours * feat: add contour-only plotting mode to CHANGELOG * ref: test zoomin contours with actual range of values and enhance test assertions
1 parent f4c27f2 commit d740fb4

6 files changed

Lines changed: 163 additions & 1 deletion

File tree

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
2.24.0
2+
- feat: add contour-only plotting mode (#210)
13
2.23.1
24
- fix: QuickView plotting failed for all-NaN coordinate data (#223)
35
- build: remove pin scipy==1.11.4

dcscope/gui/analysis/ana_plot.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ def __init__(self, *args, **kwargs):
4141
self.comboBox_division.addItem("One plot per dataset", "each")
4242
self.comboBox_division.addItem("Scatter plots and joint contour plot",
4343
"multiscatter+contour")
44+
self.comboBox_division.addItem("Only contour plots", "onlycontours")
4445
self.comboBox_division.setCurrentIndex(2)
4546

4647
# signals

dcscope/gui/pipeline_plot.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,19 @@ def update_content_plot(self, plot_state, slot_states, dslist):
224224
colspan=1)
225225
pp.redraw(dslist, slot_states, plot_state_contour)
226226

227+
elif lay["division"] == "onlycontours":
228+
# contour plots
229+
plot_state_contour = copy.deepcopy(plot_state)
230+
plot_state_contour["scatter"]["enabled"] = False
231+
pp = PipelinePlotItem(parent=linner)
232+
self.plot_items.append(pp)
233+
linner.addItem(item=pp,
234+
row=None,
235+
col=None,
236+
rowspan=1,
237+
colspan=1)
238+
pp.redraw(dslist, slot_states, plot_state_contour)
239+
227240
# colorbar
228241
colorbar_kwds = {}
229242

dcscope/pipeline/core.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,6 +545,8 @@ def get_plot_col_row_count(self, plot_id, pipeline_state=None):
545545
num_plots = 1
546546
elif div == "multiscatter+contour":
547547
num_plots = num_scat + 1
548+
elif div == "onlycontours":
549+
num_plots = 1
548550
else:
549551
raise ValueError(f"Unrecognized division: '{div}'")
550552

dcscope/pipeline/plot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
"identifier": str,
6161
"layout": {
6262
"column count": int,
63-
"division": ["each", "merge", "multiscatter+contour"],
63+
"division": ["each", "merge", "multiscatter+contour", "onlycontours"],
6464
"label plots": bool,
6565
"name": str,
6666
"size x": float,

tests/test_gui_plotting.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,3 +314,147 @@ def test_changing_lut_identifier_in_analysis_view_plots(qtbot):
314314
qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
315315

316316
assert pv.comboBox_lut.currentData() == "HE-3D-FEM-22"
317+
318+
319+
def test_zoomin_contours(qtbot):
320+
"""Test that zooming in on contours works correctly"""
321+
mw = DCscope()
322+
qtbot.addWidget(mw)
323+
324+
# Add test dataset and create plot
325+
slot_id = mw.add_dataslot(paths=[datapath / "calibration_beads_47.rtdc"])
326+
plot_id = mw.add_plot()
327+
328+
# Activate slot-plot pair
329+
pe = mw.block_matrix.get_widget(filt_plot_id=plot_id, slot_id=slot_id[0])
330+
qtbot.mouseClick(pe, QtCore.Qt.MouseButton.LeftButton)
331+
332+
# Get range before zoom-in
333+
mw.add_plot_window(plot_id)
334+
plot_widget = mw.subwindows_plots[plot_id].widget()
335+
view_range_before = plot_widget.plot_items[-1].getViewBox().viewRange()
336+
x_range_before = view_range_before[0]
337+
y_range_before = view_range_before[1]
338+
339+
# Switch to plot tab
340+
mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot)
341+
pv = mw.widget_ana_view.widget_plot
342+
343+
# Enable contour zoom-in and apply
344+
pv.checkBox_zoomin.setChecked(True)
345+
qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
346+
347+
# Get range after zoom-in
348+
plot_widget = mw.subwindows_plots[plot_id].widget()
349+
view_range_after = plot_widget.plot_items[-1].getViewBox().viewRange()
350+
x_range_after = view_range_after[0]
351+
y_range_after = view_range_after[1]
352+
353+
# Verify zoom-in reduced both X and Y ranges
354+
assert x_range_after[1] < x_range_before[1], "x-max should decrease"
355+
assert y_range_after[1] < y_range_before[1], "y-max should decrease"
356+
357+
358+
def test_only_contours_division(qtbot):
359+
"""Test that 'onlycontours' division mode works correctly"""
360+
mw = DCscope()
361+
qtbot.addWidget(mw)
362+
363+
# Add multiple datasets
364+
path = datapath / "calibration_beads_47.rtdc"
365+
mw.add_dataslot(paths=[path, path]) # Add same dataset twice for testing
366+
367+
# Add a plot
368+
plot_id = mw.add_plot()
369+
370+
# Activate analysis view
371+
pe = mw.block_matrix.get_widget(filt_plot_id=plot_id)
372+
qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton)
373+
374+
# Switch to plot tab
375+
mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot)
376+
pv = mw.widget_ana_view.widget_plot
377+
378+
# Get the initial plot state
379+
plot_state = mw.pipeline.get_plot(plot_id).__getstate__()
380+
381+
# Verify there is only one plot
382+
assert len(mw.pipeline.plot_ids) == 1, "Should have exactly one plot"
383+
384+
# Verify initial division mode
385+
assert plot_state["layout"]["division"] == "multiscatter+contour"
386+
387+
# Set division to "onlycontours"
388+
idx = pv.comboBox_division.findData("onlycontours")
389+
pv.comboBox_division.setCurrentIndex(idx)
390+
391+
# Apply changes
392+
qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
393+
394+
# Get the plot widget
395+
pw = mw.block_matrix.get_widget(filt_plot_id=plot_id)
396+
397+
# Activate plots for contour view
398+
qtbot.mouseClick(pw.toolButton_toggle, QtCore.Qt.MouseButton.LeftButton)
399+
400+
# Get the plot state
401+
plot_state = mw.pipeline.get_plot(plot_id).__getstate__()
402+
403+
# Verify division mode
404+
assert plot_state["layout"]["division"] == "onlycontours"
405+
406+
407+
def test_contour_plot_with_invalid_percentiles(qtbot):
408+
"""Test contour plot with edge case percentiles (e.g., 100% KDE)"""
409+
mw = DCscope()
410+
qtbot.addWidget(mw)
411+
412+
# Add a dataset
413+
path = datapath / "calibration_beads_47.rtdc"
414+
slot_id = mw.add_dataslot(paths=[path])[0]
415+
416+
# Add a plot
417+
plot_id = mw.add_plot()
418+
419+
# Activate the slot-plot pair to show data
420+
pe = mw.block_matrix.get_widget(slot_id, plot_id)
421+
qtbot.mouseClick(pe, QtCore.Qt.MouseButton.LeftButton)
422+
423+
# Activate analysis view
424+
pe = mw.block_matrix.get_widget(filt_plot_id=plot_id)
425+
qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton)
426+
427+
# Switch to plot tab
428+
mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot)
429+
pv = mw.widget_ana_view.widget_plot
430+
431+
# Enable contours
432+
pv.groupBox_contour.setChecked(True)
433+
434+
# Set contour percentiles to extreme values (edge cases)
435+
# 100% percentile is at the maximum KDE value
436+
pv.doubleSpinBox_perc_1.setValue(100.0) # Maximum percentile
437+
pv.doubleSpinBox_perc_2.setValue(100.0) # Near maximum
438+
439+
# Apply changes
440+
qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton)
441+
442+
# Verify the plot state was updated with the new percentiles
443+
plot_state = mw.pipeline.get_plot(plot_id).__getstate__()
444+
con = plot_state["contour"]
445+
446+
# Check that percentiles were set
447+
assert con["percentiles"][0] == 100.0
448+
assert con["percentiles"][1] == 100.0
449+
assert con["enabled"] is True
450+
451+
# Open the plot window to verify rendering works
452+
mw.add_plot_window(plot_id)
453+
454+
# Get the plot widget
455+
plot_widget = mw.subwindows_plots[plot_id].widget()
456+
457+
# Check that plot items were created
458+
assert plot_widget is not None
459+
if plot_widget.plot_items:
460+
assert len(plot_widget.plot_items) > 0

0 commit comments

Comments
 (0)