From 0701a010775eb5504f4b03b50624941c07d5d059 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Tue, 23 Jun 2026 13:52:29 +0200 Subject: [PATCH 1/3] enh: show r^2 and make fit line togglable --- pyjibe/fd/main.py | 1 + pyjibe/fd/main.ui | 10 ++++++++++ pyjibe/fd/mpl_indent.py | 20 ++++++++++++++++++-- pyjibe/fd/widget_plot_fd.py | 4 +++- 4 files changed, 32 insertions(+), 3 deletions(-) diff --git a/pyjibe/fd/main.py b/pyjibe/fd/main.py index 35c27b9..df1735c 100644 --- a/pyjibe/fd/main.py +++ b/pyjibe/fd/main.py @@ -83,6 +83,7 @@ def __init__(self, *args, **kwargs): self.on_cb_rating_scheme) self.btn_rater.clicked.connect(self.on_user_rate) # plotting parameters + self.cb_show_fit_line.stateChanged.connect(self.on_mpl_curve_update) self.cb_mpl_rescale_plot_x.stateChanged.connect( self.on_mpl_curve_update) self.cb_mpl_rescale_plot_x_min.valueChanged.connect( diff --git a/pyjibe/fd/main.ui b/pyjibe/fd/main.ui index 0bddabc..bcc710b 100644 --- a/pyjibe/fd/main.ui +++ b/pyjibe/fd/main.ui @@ -138,6 +138,16 @@ + + + + Show fit line + + + true + + + diff --git a/pyjibe/fd/mpl_indent.py b/pyjibe/fd/mpl_indent.py index 5714f4e..0dc2e70 100644 --- a/pyjibe/fd/mpl_indent.py +++ b/pyjibe/fd/mpl_indent.py @@ -50,6 +50,11 @@ def __init__(self): range(2), label="residuals")[0] + self.ann_r2 = self.axis_main.text( + 0.98, 0.95, "", transform=self.axis_main.transAxes, + ha="right", va="top", fontsize=9, + bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.7)) + self.canvas = FigureCanvas(self.figure) self.canvas.draw() @@ -67,7 +72,7 @@ def add_toolbar(self, widget): def save_data_callback(self, filename): self.fdist.export(filename) - def update(self, fdist, rescale_x=None, rescale_y=None): + def update(self, fdist, rescale_x=None, rescale_y=None, show_fit=True): self.fdist = fdist xaxis = "tip position" yaxis = "force" @@ -96,13 +101,22 @@ def update(self, fdist, rescale_x=None, rescale_y=None): if "fit" in fdist and np.sum(fdist["fit range"]): self.plots["residuals"].set_visible(True) - self.plots["fit"].set_visible(True) + self.plots["fit"].set_visible(show_fit) self.plots["fit range"].set_visible(True) self.plots["fit"].set_data(fdist["tip position"]*xscale, fdist["fit"]*yscale) self.plots["residuals"].set_data(fdist["tip position"]*xscale, (fdist["fit residuals"])*yscale) + + fit_mask = fdist["fit range"] + y_true = fdist["force"][fit_mask] + y_pred = fdist["fit"][fit_mask] + ss_res = np.sum((y_true - y_pred) ** 2) + ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) + r2 = 1 - ss_res / ss_tot if ss_tot != 0 else np.nan + self.ann_r2.set_text(f"R² = {r2:.4f}") + self.ann_r2.set_visible(True) # fit range fitrange = (fdist[xaxis]*xscale)[fdist["fit range"]] fitmin = np.min(fitrange) @@ -132,6 +146,8 @@ def update(self, fdist, rescale_x=None, rescale_y=None): self.plots["residuals"].set_visible(False) self.plots["fit"].set_visible(False) self.plots["fit range"].set_visible(False) + self.ann_r2.set_text("") + self.ann_r2.set_visible(False) self.canvas.draw() def update_plot(self, rescale_x=None, rescale_y=None): diff --git a/pyjibe/fd/widget_plot_fd.py b/pyjibe/fd/widget_plot_fd.py index 65f4105..0eeab9b 100644 --- a/pyjibe/fd/widget_plot_fd.py +++ b/pyjibe/fd/widget_plot_fd.py @@ -36,6 +36,8 @@ def mpl_curve_update(self, fdist): rescale_y = (self.fd.cb_mpl_rescale_plot_y_min.value(), self.fd.cb_mpl_rescale_plot_y_max.value()) + show_fit = self.fd.cb_show_fit_line.isChecked() self.mpl_curve.update(fdist, rescale_x=rescale_x, - rescale_y=rescale_y) + rescale_y=rescale_y, + show_fit=show_fit) From e96249946719d29263c75e97bb65b4365b99bc96 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Tue, 23 Jun 2026 13:53:11 +0200 Subject: [PATCH 2/3] tests: check that the fit line is toggable --- CHANGELOG | 1 + tests/test_fd_fit.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index fda13cc..b9627b6 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,5 @@ 0.16.4 + - enh: show R^2 annotation and toggle fit line visibility (#48, #52) - setup: support latest python versions (#32, #46) - setup: support latest matplotlib versions (#32, #46) - fix: remember user's choice upon curve open (#44, #45) diff --git a/tests/test_fd_fit.py b/tests/test_fd_fit.py index 5f37733..9d04dbc 100644 --- a/tests/test_fd_fit.py +++ b/tests/test_fd_fit.py @@ -262,3 +262,50 @@ def test_set_indentation_depth_manually_infdoublespinbox(qtbot): qtbot.keyClicks(war.tab_fit.sp_range_1, text_entered) assert war.tab_fit.sp_range_1.value() == resulting_value main_window.close() + + +def test_show_fit_line_toggle(qtbot): + """Toggle cb_show_fit_line hides/shows the fit line on the plot.""" + main_window = pyjibe.head.PyJibe() + qtbot.addWidget(main_window) + main_window.load_data(files=make_directory_with_data(2)) + war = main_window.subwindows[0].widget() + war.cb_autosave.setChecked(0) + war.tab_preprocess.set_preprocessing(["compute_tip_position"]) + war.tab_fit.cb_weight_cp.setCheckState(QtCore.Qt.CheckState.Unchecked) + war.on_tab_changed() + + mpl = war.widget_plot_fd.mpl_curve + # fit line should be visible by default + assert mpl.plots["fit"].get_visible() + + # uncheck the toggle — fit line should disappear + war.cb_show_fit_line.setChecked(False) + war.on_mpl_curve_update() + assert not mpl.plots["fit"].get_visible() + + # re-check — fit line should reappear + war.cb_show_fit_line.setChecked(True) + war.on_mpl_curve_update() + assert mpl.plots["fit"].get_visible() + main_window.close() + + +def test_r2_annotation_shown_after_fit(qtbot): + """R² annotation is visible and well-formed after a successful fit.""" + main_window = pyjibe.head.PyJibe() + qtbot.addWidget(main_window) + main_window.load_data(files=make_directory_with_data(2)) + war = main_window.subwindows[0].widget() + war.cb_autosave.setChecked(0) + war.tab_preprocess.set_preprocessing(["compute_tip_position"]) + war.tab_fit.cb_weight_cp.setCheckState(QtCore.Qt.CheckState.Unchecked) + war.on_tab_changed() + + mpl = war.widget_plot_fd.mpl_curve + assert mpl.ann_r2.get_visible() + text = mpl.ann_r2.get_text() + assert text.startswith("R²") + r2_value = float(text.split("=")[1].strip()) + assert 0.0 <= r2_value <= 1.0 + main_window.close() From d6304fa5ab6b0a692d5e6c6eb5795f2e07bf51a0 Mon Sep 17 00:00:00 2001 From: Eoghan O'Connell Date: Mon, 29 Jun 2026 10:49:32 +0200 Subject: [PATCH 3/3] ref: change to chi_sqr from nanite fit metadata --- CHANGELOG | 2 +- pyjibe/fd/mpl_indent.py | 17 ++++++----------- tests/test_fd_fit.py | 14 +++++++------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index b9627b6..e0a9451 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ 0.16.4 - - enh: show R^2 annotation and toggle fit line visibility (#48, #52) + - enh: show chi^2 annotation and toggle fit line visibility (#48, #52) - setup: support latest python versions (#32, #46) - setup: support latest matplotlib versions (#32, #46) - fix: remember user's choice upon curve open (#44, #45) diff --git a/pyjibe/fd/mpl_indent.py b/pyjibe/fd/mpl_indent.py index 0dc2e70..358b83d 100644 --- a/pyjibe/fd/mpl_indent.py +++ b/pyjibe/fd/mpl_indent.py @@ -50,7 +50,7 @@ def __init__(self): range(2), label="residuals")[0] - self.ann_r2 = self.axis_main.text( + self.ann_chi2 = self.axis_main.text( 0.98, 0.95, "", transform=self.axis_main.transAxes, ha="right", va="top", fontsize=9, bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.7)) @@ -109,14 +109,9 @@ def update(self, fdist, rescale_x=None, rescale_y=None, show_fit=True): self.plots["residuals"].set_data(fdist["tip position"]*xscale, (fdist["fit residuals"])*yscale) - fit_mask = fdist["fit range"] - y_true = fdist["force"][fit_mask] - y_pred = fdist["fit"][fit_mask] - ss_res = np.sum((y_true - y_pred) ** 2) - ss_tot = np.sum((y_true - np.mean(y_true)) ** 2) - r2 = 1 - ss_res / ss_tot if ss_tot != 0 else np.nan - self.ann_r2.set_text(f"R² = {r2:.4f}") - self.ann_r2.set_visible(True) + chi_sqr = fdist.fit_properties["chi_sqr"] + self.ann_chi2.set_text(rf"$\chi^2$ = {chi_sqr:.2e}") + self.ann_chi2.set_visible(True) # fit range fitrange = (fdist[xaxis]*xscale)[fdist["fit range"]] fitmin = np.min(fitrange) @@ -146,8 +141,8 @@ def update(self, fdist, rescale_x=None, rescale_y=None, show_fit=True): self.plots["residuals"].set_visible(False) self.plots["fit"].set_visible(False) self.plots["fit range"].set_visible(False) - self.ann_r2.set_text("") - self.ann_r2.set_visible(False) + self.ann_chi2.set_text("") + self.ann_chi2.set_visible(False) self.canvas.draw() def update_plot(self, rescale_x=None, rescale_y=None): diff --git a/tests/test_fd_fit.py b/tests/test_fd_fit.py index 9d04dbc..f5e9ac9 100644 --- a/tests/test_fd_fit.py +++ b/tests/test_fd_fit.py @@ -291,8 +291,8 @@ def test_show_fit_line_toggle(qtbot): main_window.close() -def test_r2_annotation_shown_after_fit(qtbot): - """R² annotation is visible and well-formed after a successful fit.""" +def test_chi_sqr_annotation_shown_after_fit(qtbot): + """Chi^2 annotation should be visible after a successful fit.""" main_window = pyjibe.head.PyJibe() qtbot.addWidget(main_window) main_window.load_data(files=make_directory_with_data(2)) @@ -303,9 +303,9 @@ def test_r2_annotation_shown_after_fit(qtbot): war.on_tab_changed() mpl = war.widget_plot_fd.mpl_curve - assert mpl.ann_r2.get_visible() - text = mpl.ann_r2.get_text() - assert text.startswith("R²") - r2_value = float(text.split("=")[1].strip()) - assert 0.0 <= r2_value <= 1.0 + assert mpl.ann_chi2.get_visible() + text = mpl.ann_chi2.get_text() + assert text.startswith(r"$\chi^2$") + chi2_value = float(text.split("=")[1].strip()) + assert 0.0 <= chi2_value <= 1.0 main_window.close()