From d18171592fe211c071eecdf447d1927635680213 Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:33:51 +0200 Subject: [PATCH 1/9] Add onlycontours option in dropdown --- dcscope/gui/analysis/ana_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dcscope/gui/analysis/ana_plot.py b/dcscope/gui/analysis/ana_plot.py index 4294e1f..cdef704 100644 --- a/dcscope/gui/analysis/ana_plot.py +++ b/dcscope/gui/analysis/ana_plot.py @@ -41,6 +41,7 @@ def __init__(self, *args, **kwargs): self.comboBox_division.addItem("One plot per dataset", "each") self.comboBox_division.addItem("Scatter plots and joint contour plot", "multiscatter+contour") + self.comboBox_division.addItem("Only contour plots", "onlycontours") self.comboBox_division.setCurrentIndex(2) # signals From d03d9952aa6d944f0bb8e378a8ce090b7c5575f4 Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:34:58 +0200 Subject: [PATCH 2/9] Update STATE_OPTIONS with onlycontours --- dcscope/pipeline/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcscope/pipeline/plot.py b/dcscope/pipeline/plot.py index bd75727..2a561d8 100644 --- a/dcscope/pipeline/plot.py +++ b/dcscope/pipeline/plot.py @@ -60,7 +60,7 @@ "identifier": str, "layout": { "column count": int, - "division": ["each", "merge", "multiscatter+contour"], + "division": ["each", "merge", "multiscatter+contour", "onlycontours"], "label plots": bool, "name": str, "size x": float, From 78060ef514330d9184d67eaa4ea4e3672f7de9e2 Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:35:25 +0200 Subject: [PATCH 3/9] Add support for 'onlycontours' division in plot count calculation --- dcscope/gui/pipeline_plot.py | 13 +++++++++++++ dcscope/pipeline/core.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/dcscope/gui/pipeline_plot.py b/dcscope/gui/pipeline_plot.py index cac4eb4..317f9e6 100644 --- a/dcscope/gui/pipeline_plot.py +++ b/dcscope/gui/pipeline_plot.py @@ -224,6 +224,19 @@ def update_content_plot(self, plot_state, slot_states, dslist): colspan=1) pp.redraw(dslist, slot_states, plot_state_contour) + elif lay["division"] == "onlycontours": + # contour plots + plot_state_contour = copy.deepcopy(plot_state) + plot_state_contour["scatter"]["enabled"] = False + pp = PipelinePlotItem(parent=linner) + self.plot_items.append(pp) + linner.addItem(item=pp, + row=None, + col=None, + rowspan=1, + colspan=1) + pp.redraw(dslist, slot_states, plot_state_contour) + # colorbar colorbar_kwds = {} diff --git a/dcscope/pipeline/core.py b/dcscope/pipeline/core.py index 52e7b2e..699efd5 100644 --- a/dcscope/pipeline/core.py +++ b/dcscope/pipeline/core.py @@ -545,6 +545,8 @@ def get_plot_col_row_count(self, plot_id, pipeline_state=None): num_plots = 1 elif div == "multiscatter+contour": num_plots = num_scat + 1 + elif div == "onlycontours": + num_plots = 1 else: raise ValueError(f"Unrecognized division: '{div}'") From ede7e5dc692869cba96ef88cbd9d5882c40ffe8f Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:33:51 +0200 Subject: [PATCH 4/9] Add onlycontours option in dropdown --- dcscope/gui/analysis/ana_plot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dcscope/gui/analysis/ana_plot.py b/dcscope/gui/analysis/ana_plot.py index 4294e1f..cdef704 100644 --- a/dcscope/gui/analysis/ana_plot.py +++ b/dcscope/gui/analysis/ana_plot.py @@ -41,6 +41,7 @@ def __init__(self, *args, **kwargs): self.comboBox_division.addItem("One plot per dataset", "each") self.comboBox_division.addItem("Scatter plots and joint contour plot", "multiscatter+contour") + self.comboBox_division.addItem("Only contour plots", "onlycontours") self.comboBox_division.setCurrentIndex(2) # signals From 825322c052a0b6b0c878da1afb7054b0098de34e Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:34:58 +0200 Subject: [PATCH 5/9] Update STATE_OPTIONS with onlycontours --- dcscope/pipeline/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcscope/pipeline/plot.py b/dcscope/pipeline/plot.py index bd75727..2a561d8 100644 --- a/dcscope/pipeline/plot.py +++ b/dcscope/pipeline/plot.py @@ -60,7 +60,7 @@ "identifier": str, "layout": { "column count": int, - "division": ["each", "merge", "multiscatter+contour"], + "division": ["each", "merge", "multiscatter+contour", "onlycontours"], "label plots": bool, "name": str, "size x": float, From e262b5440bff21d69a8916e2d9b79fca5a5dd992 Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Tue, 21 Oct 2025 14:35:25 +0200 Subject: [PATCH 6/9] Add support for 'onlycontours' division in plot count calculation --- dcscope/gui/pipeline_plot.py | 13 +++++++++++++ dcscope/pipeline/core.py | 2 ++ 2 files changed, 15 insertions(+) diff --git a/dcscope/gui/pipeline_plot.py b/dcscope/gui/pipeline_plot.py index cac4eb4..317f9e6 100644 --- a/dcscope/gui/pipeline_plot.py +++ b/dcscope/gui/pipeline_plot.py @@ -224,6 +224,19 @@ def update_content_plot(self, plot_state, slot_states, dslist): colspan=1) pp.redraw(dslist, slot_states, plot_state_contour) + elif lay["division"] == "onlycontours": + # contour plots + plot_state_contour = copy.deepcopy(plot_state) + plot_state_contour["scatter"]["enabled"] = False + pp = PipelinePlotItem(parent=linner) + self.plot_items.append(pp) + linner.addItem(item=pp, + row=None, + col=None, + rowspan=1, + colspan=1) + pp.redraw(dslist, slot_states, plot_state_contour) + # colorbar colorbar_kwds = {} diff --git a/dcscope/pipeline/core.py b/dcscope/pipeline/core.py index 52e7b2e..699efd5 100644 --- a/dcscope/pipeline/core.py +++ b/dcscope/pipeline/core.py @@ -545,6 +545,8 @@ def get_plot_col_row_count(self, plot_id, pipeline_state=None): num_plots = 1 elif div == "multiscatter+contour": num_plots = num_scat + 1 + elif div == "onlycontours": + num_plots = 1 else: raise ValueError(f"Unrecognized division: '{div}'") From a29913db557bb9b6beaebeaba951895aa976894d Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Mon, 29 Dec 2025 15:38:45 +0100 Subject: [PATCH 7/9] Add tests for 'onlycontours' division mode and zooming in on contours --- tests/test_gui_plotting.py | 156 +++++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/tests/test_gui_plotting.py b/tests/test_gui_plotting.py index 627d0f1..446959e 100644 --- a/tests/test_gui_plotting.py +++ b/tests/test_gui_plotting.py @@ -314,3 +314,159 @@ def test_changing_lut_identifier_in_analysis_view_plots(qtbot): qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) assert pv.comboBox_lut.currentData() == "HE-3D-FEM-22" + + +def test_zoomin_contours(qtbot): + """Test that zooming in on contours works correctly""" + mw = DCscope() + qtbot.addWidget(mw) + + # Add test datasets + paths = [ + datapath / "artificial_with_image_bg.rtdc", + datapath / "blood_rbc_leukocytes.rtdc", + datapath / "calibration_beads_47.rtdc" + ] + + mw.add_dataslot(paths=paths) + + # Add a plot + plot_id = mw.add_plot() + + # Activate analysis view + pe = mw.block_matrix.get_widget(filt_plot_id=plot_id) + qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton) + + # Switch to plot tab + mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot) + pv = mw.widget_ana_view.widget_plot + + # Enable contours and zoom-in + # pv.checkBox_contour.setChecked(True) + pv.checkBox_zoomin.setChecked(True) + + # Apply changes + qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) + + # Open the plot window + mw.add_plot_window(plot_id) + + # Get the plot widget + plot_widget = mw.subwindows_plots[plot_id].widget() + + # Get the plot items from the pipeline plot + if plot_widget.plot_items: + # Get the first plot item (in case there are multiple due to division) + plot_item = plot_widget.plot_items[0] + + # Get the current view range + x_range = plot_item.getViewBox().viewRange()[0] + y_range = plot_item.getViewBox().viewRange()[1] + + # Verify that zoom was applied + assert np.all(np.isfinite(x_range)) + assert np.all(np.isfinite(y_range)) + assert x_range[0] != 0 or x_range[1] != 1 + assert y_range[0] != 0 or y_range[1] != 1 + + +def test_only_contours_division(qtbot): + """Test that 'onlycontours' division mode works correctly""" + mw = DCscope() + qtbot.addWidget(mw) + + # Add multiple datasets + path = datapath / "calibration_beads_47.rtdc" + mw.add_dataslot(paths=[path, path]) # Add same dataset twice for testing + + # Add a plot + plot_id = mw.add_plot() + + # Activate analysis view + pe = mw.block_matrix.get_widget(filt_plot_id=plot_id) + qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton) + + # Switch to plot tab + mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot) + pv = mw.widget_ana_view.widget_plot + + # Get the initial plot state + plot_state = mw.pipeline.get_plot(plot_id).__getstate__() + + # Verify initial division mode + assert plot_state["layout"]["division"] == "multiscatter+contour" + + # Set division to "onlycontours" + idx = pv.comboBox_division.findData("onlycontours") + pv.comboBox_division.setCurrentIndex(idx) + + # Apply changes + qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) + + # Get the plot widget + pw = mw.block_matrix.get_widget(filt_plot_id=plot_id) + + # Activate plots for contour view + qtbot.mouseClick(pw.toolButton_toggle, QtCore.Qt.MouseButton.LeftButton) + + # Get the plot state + plot_state = mw.pipeline.get_plot(plot_id).__getstate__() + + # Verify division mode + assert plot_state["layout"]["division"] == "onlycontours" + + +def test_contour_plot_with_invalid_percentiles(qtbot): + """Test contour plot with edge case percentiles (e.g., 100% KDE)""" + mw = DCscope() + qtbot.addWidget(mw) + + # Add a dataset + path = datapath / "calibration_beads_47.rtdc" + slot_id = mw.add_dataslot(paths=[path])[0] + + # Add a plot + plot_id = mw.add_plot() + + # Activate the slot-plot pair to show data + pe = mw.block_matrix.get_widget(slot_id, plot_id) + qtbot.mouseClick(pe, QtCore.Qt.MouseButton.LeftButton) + + # Activate analysis view + pe = mw.block_matrix.get_widget(filt_plot_id=plot_id) + qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton) + + # Switch to plot tab + mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot) + pv = mw.widget_ana_view.widget_plot + + # Enable contours + pv.groupBox_contour.setChecked(True) + + # Set contour percentiles to extreme values (edge cases) + # 100% percentile is at the maximum KDE value + pv.doubleSpinBox_perc_1.setValue(100.0) # Maximum percentile + pv.doubleSpinBox_perc_2.setValue(99.0) # Near maximum + + # Apply changes + qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) + + # Verify the plot state was updated with the new percentiles + plot_state = mw.pipeline.get_plot(plot_id).__getstate__() + con = plot_state["contour"] + + # Check that percentiles were set + assert con["percentiles"][0] == 100.0 + assert con["percentiles"][1] == 99.0 + assert con["enabled"] is True + + # Open the plot window to verify rendering works + mw.add_plot_window(plot_id) + + # Get the plot widget + plot_widget = mw.subwindows_plots[plot_id].widget() + + # Check that plot items were created + assert plot_widget is not None + if plot_widget.plot_items: + assert len(plot_widget.plot_items) > 0 From e8e8f0091391ae0180ca08ac961de0521203adff Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Fri, 2 Jan 2026 13:13:38 +0100 Subject: [PATCH 8/9] feat: add contour-only plotting mode to CHANGELOG --- CHANGELOG | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1319c71..6cb1c7e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +2.24.0 + - feat: add contour-only plotting mode (#210) 2.23.1 - fix: QuickView plotting failed for all-NaN coordinate data (#223) - build: remove pin scipy==1.11.4 From b07dfdd41daa98d1e897d24a098b2f565ae01105 Mon Sep 17 00:00:00 2001 From: RaghavaAlajangi Date: Wed, 7 Jan 2026 15:42:12 +0100 Subject: [PATCH 9/9] ref: test zoomin contours with actual range of values and enhance test assertions --- tests/test_gui_plotting.py | 62 +++++++++++++++----------------------- 1 file changed, 25 insertions(+), 37 deletions(-) diff --git a/tests/test_gui_plotting.py b/tests/test_gui_plotting.py index 446959e..fd4e854 100644 --- a/tests/test_gui_plotting.py +++ b/tests/test_gui_plotting.py @@ -321,53 +321,38 @@ def test_zoomin_contours(qtbot): mw = DCscope() qtbot.addWidget(mw) - # Add test datasets - paths = [ - datapath / "artificial_with_image_bg.rtdc", - datapath / "blood_rbc_leukocytes.rtdc", - datapath / "calibration_beads_47.rtdc" - ] - - mw.add_dataslot(paths=paths) - - # Add a plot + # Add test dataset and create plot + slot_id = mw.add_dataslot(paths=[datapath / "calibration_beads_47.rtdc"]) plot_id = mw.add_plot() - # Activate analysis view - pe = mw.block_matrix.get_widget(filt_plot_id=plot_id) - qtbot.mouseClick(pe.toolButton_modify, QtCore.Qt.MouseButton.LeftButton) + # Activate slot-plot pair + pe = mw.block_matrix.get_widget(filt_plot_id=plot_id, slot_id=slot_id[0]) + qtbot.mouseClick(pe, QtCore.Qt.MouseButton.LeftButton) + + # Get range before zoom-in + mw.add_plot_window(plot_id) + plot_widget = mw.subwindows_plots[plot_id].widget() + view_range_before = plot_widget.plot_items[-1].getViewBox().viewRange() + x_range_before = view_range_before[0] + y_range_before = view_range_before[1] # Switch to plot tab mw.widget_ana_view.tabWidget.setCurrentWidget(mw.widget_ana_view.tab_plot) pv = mw.widget_ana_view.widget_plot - # Enable contours and zoom-in - # pv.checkBox_contour.setChecked(True) + # Enable contour zoom-in and apply pv.checkBox_zoomin.setChecked(True) - - # Apply changes qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) - # Open the plot window - mw.add_plot_window(plot_id) - - # Get the plot widget + # Get range after zoom-in plot_widget = mw.subwindows_plots[plot_id].widget() + view_range_after = plot_widget.plot_items[-1].getViewBox().viewRange() + x_range_after = view_range_after[0] + y_range_after = view_range_after[1] - # Get the plot items from the pipeline plot - if plot_widget.plot_items: - # Get the first plot item (in case there are multiple due to division) - plot_item = plot_widget.plot_items[0] - - # Get the current view range - x_range = plot_item.getViewBox().viewRange()[0] - y_range = plot_item.getViewBox().viewRange()[1] - - # Verify that zoom was applied - assert np.all(np.isfinite(x_range)) - assert np.all(np.isfinite(y_range)) - assert x_range[0] != 0 or x_range[1] != 1 - assert y_range[0] != 0 or y_range[1] != 1 + # Verify zoom-in reduced both X and Y ranges + assert x_range_after[1] < x_range_before[1], "x-max should decrease" + assert y_range_after[1] < y_range_before[1], "y-max should decrease" def test_only_contours_division(qtbot): @@ -393,6 +378,9 @@ def test_only_contours_division(qtbot): # Get the initial plot state plot_state = mw.pipeline.get_plot(plot_id).__getstate__() + # Verify there is only one plot + assert len(mw.pipeline.plot_ids) == 1, "Should have exactly one plot" + # Verify initial division mode assert plot_state["layout"]["division"] == "multiscatter+contour" @@ -446,7 +434,7 @@ def test_contour_plot_with_invalid_percentiles(qtbot): # Set contour percentiles to extreme values (edge cases) # 100% percentile is at the maximum KDE value pv.doubleSpinBox_perc_1.setValue(100.0) # Maximum percentile - pv.doubleSpinBox_perc_2.setValue(99.0) # Near maximum + pv.doubleSpinBox_perc_2.setValue(100.0) # Near maximum # Apply changes qtbot.mouseClick(pv.pushButton_apply, QtCore.Qt.MouseButton.LeftButton) @@ -457,7 +445,7 @@ def test_contour_plot_with_invalid_percentiles(qtbot): # Check that percentiles were set assert con["percentiles"][0] == 100.0 - assert con["percentiles"][1] == 99.0 + assert con["percentiles"][1] == 100.0 assert con["enabled"] is True # Open the plot window to verify rendering works