From 4d43a8800cc8d92531b756ce625c44d2fe4efe08 Mon Sep 17 00:00:00 2001 From: Ross Blair Date: Wed, 15 May 2024 08:40:27 -0500 Subject: [PATCH 01/30] set max height and overflow css for qcrating widget --- nireports/assembler/data/rating-widget/bootstrap.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/assembler/data/rating-widget/bootstrap.yml b/nireports/assembler/data/rating-widget/bootstrap.yml index 5d73c77b..d41a9d03 100644 --- a/nireports/assembler/data/rating-widget/bootstrap.yml +++ b/nireports/assembler/data/rating-widget/bootstrap.yml @@ -7,7 +7,7 @@ defaults: endpoint: "https://localhost:8000/" access_token: settings: - style: 'width: 30%; top: 100px; left: 65%;' + style: 'width: 30%; top: 100px; left: 65%; max-height: 85%; overflow-y: auto;' navbar_label: Rating widget mintime: 10 components: From b4b65a970d753bb406b85e8b7a913e8ce9922234 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 1 Jul 2024 15:21:47 +0200 Subject: [PATCH 02/30] sty: pacify ruff --- nireports/reportlets/nuisance.py | 2 +- pyproject.toml | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/nireports/reportlets/nuisance.py b/nireports/reportlets/nuisance.py index 444104ec..a8cbf243 100644 --- a/nireports/reportlets/nuisance.py +++ b/nireports/reportlets/nuisance.py @@ -339,7 +339,7 @@ def plot_carpet( ) label = "" - for i, (label, indices) in enumerate(segments.items()): + for i, (_, indices) in enumerate(segments.items()): # Carpet plot ax = plt.subplot(gs[i]) diff --git a/pyproject.toml b/pyproject.toml index 44ef88db..ef7d0438 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,8 @@ select = [ ] ignore = [ "E203", + "B019", + "C901", ] [tool.ruff.lint.flake8-quotes] From fcfb2263c1c7e7873e48f14a048a5cfc0a17e585 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 1 Jul 2024 15:41:28 +0200 Subject: [PATCH 03/30] MAINT: Pin numpy < 2.0 --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index ef7d0438..297e4bc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "nibabel >= 3.0.1", "nilearn >= 0.5.2", "nipype", - "numpy", + "numpy < 2.0", "pandas", "pybids", "pyyaml", From b8337fb1b6de5b904fa8e51f1a93e78390f1b849 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20Haitz=20Legarreta=20Gorro=C3=B1o?= Date: Wed, 8 May 2024 20:18:13 -0400 Subject: [PATCH 04/30] ENH: Add raincloud plot capabilities Add raincloud plot capabilities and demonstrate on the FA values of computed on a DWI volume across different tissue types. Add the corresponding tests. Add a `utils.py` helper module to the `tests` module. Contains a function to create random data used by the added tests. --- nireports/interfaces/__init__.py | 7 +- nireports/interfaces/nuisance.py | 108 ++++++++++- nireports/reportlets/modality/dwi.py | 28 +++ nireports/reportlets/nuisance.py | 280 ++++++++++++++++++++++++++- nireports/tests/test_dwi.py | 42 +++- nireports/tests/test_interfaces.py | 42 +++- nireports/tests/test_reportlets.py | 105 +++++++++- nireports/tests/utils.py | 37 ++++ 8 files changed, 640 insertions(+), 9 deletions(-) create mode 100644 nireports/tests/utils.py diff --git a/nireports/interfaces/__init__.py b/nireports/interfaces/__init__.py index e190ef32..7445a1ac 100644 --- a/nireports/interfaces/__init__.py +++ b/nireports/interfaces/__init__.py @@ -24,11 +24,16 @@ from nireports.interfaces.fmri import FMRISummary from nireports.interfaces.mosaic import PlotContours, PlotMosaic, PlotSpikes -from nireports.interfaces.nuisance import CompCorVariancePlot, ConfoundsCorrelationPlot +from nireports.interfaces.nuisance import ( + CompCorVariancePlot, + ConfoundsCorrelationPlot, + RaincloudPlot, +) __all__ = ( "CompCorVariancePlot", "ConfoundsCorrelationPlot", + "RaincloudPlot", "FMRISummary", "PlotContours", "PlotMosaic", diff --git a/nireports/interfaces/nuisance.py b/nireports/interfaces/nuisance.py index 734767f4..bbb032b1 100644 --- a/nireports/interfaces/nuisance.py +++ b/nireports/interfaces/nuisance.py @@ -32,7 +32,7 @@ ) from nipype.utils.filemanip import fname_presuffix -from nireports.reportlets.nuisance import confounds_correlation_plot +from nireports.reportlets.nuisance import confounds_correlation_plot, plot_raincloud from nireports.reportlets.xca import compcor_variance_plot @@ -142,3 +142,109 @@ def _run_interface(self, runtime): ignore_initial_volumes=self.inputs.ignore_initial_volumes, ) return runtime + + +class _RaincloudPlotInputSpec(BaseInterfaceInputSpec): + data_file = File(exists=True, mandatory=True, desc="File containing the data") + out_file = traits.Either(None, File, value=None, usedefault=True, desc="Path to save plot") + group_name = traits.Str( + "group_name", + mandatory=True, + desc="Group name of interest", + ) + feature = traits.Str( + "feature", + mandatory=True, + desc="Feature of interest", + ) + palette = traits.Str( + "Set2", + usedefault=True, + desc="Color palette name", + ) + orient = traits.Str( + "v", + usedefault=True, + desc="Orientation", + ) + density = traits.Bool( + True, + usedefault=True, + desc="``True`` to plot the density", + ) + upper_limit_value = traits.Float( + None, + usedefault=True, + desc="Upper limit value over which any value in the data will be styled " + "with a different style", + ) + upper_limit_color = traits.Str( + "gray", + usedefault=True, + desc="Lower limit value under which any value in the data will be styled " + "with a different style", + ) + lower_limit_value = traits.Float( + None, + usedefault=True, + desc="", + ) + lower_limit_color = traits.Str( + "gray", + usedefault=True, + desc="Color name to represent values under ``lower_limit_value``", + ) + limit_offset = traits.Float( + None, + usedefault=True, + desc="Offset to plot the values over/under the upper/lower limit values", + ) + mark_nans = traits.Bool( + True, + usedefault=True, + desc="``True`` to plot NaNs as dots. ``nans_values`` must be provided if True", + ) + nans_value = traits.Float( + None, + usedefault=True, + desc="Value to use for NaN values`", + ) + nans_color = traits.Str( + "black", + usedefault=True, + desc="Color name to represent NaN values", + ) + + +class _RaincloudPlotOutputSpec(TraitedSpec): + out_file = File(exists=True, desc="Path to saved plot") + + +class RaincloudPlot(SimpleInterface): + """Plot a raincloud of values.""" + + input_spec = _RaincloudPlotInputSpec + output_spec = _RaincloudPlotOutputSpec + + def _run_interface(self, runtime, **kwargs): + if self.inputs.out_file is None: + self._results["out_file"] = fname_presuffix( + self.inputs.data_file, + suffix="_raincloud.svg", + use_ext=False, + newpath=runtime.cwd, + ) + else: + self._results["out_file"] = self.inputs.out_file + plot_raincloud( + data_file=self.inputs.data_file, + group_name=self.inputs.group_name, + feature=self.inputs.feature, + palette=self.inputs.palette, + orient=self.inputs.orient, + density=self.inputs.density, + mark_nans=self.inputs.mark_nans, + output_file=self._results["out_file"], + **kwargs, + ) + return runtime diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 117b16f5..a42ce57b 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -29,6 +29,8 @@ from mpl_toolkits.mplot3d import art3d from nilearn.plotting import plot_anat +from nireports.reportlets.nuisance import plot_raincloud + def plot_dwi(dataobj, affine, gradient=None, **kwargs): """ @@ -405,3 +407,29 @@ def plot_gradients( plt.suptitle(title) return ax + + +def plot_tissue_values(data_file, group_name, feature, **kwargs): + """Generate a raincloud plot with the data points corresponding to the + ``feature`` value contained in the data file. + + Parameters + ---------- + data_file : :obj:`str` + File containing the data of interest. + group_name : :obj:`str` + The group name of interest to be plot. + feature : :obj:`str` + The feature of interest to be plot. + kwargs : :obj:`dict` + Extra args given to :func:~`nireports.reportlets.nuisance.plot_raincloud`. + + Returns + ------- + axes and gridspec + Plotting axes and gridspec. Returned only if ``output_file`` is ``None``. + output_file : :obj:`str` + The file where the figure is saved. + """ + + return plot_raincloud(data_file, group_name, feature, **kwargs) diff --git a/nireports/reportlets/nuisance.py b/nireports/reportlets/nuisance.py index a8cbf243..e9b58714 100644 --- a/nireports/reportlets/nuisance.py +++ b/nireports/reportlets/nuisance.py @@ -25,11 +25,13 @@ """Plotting distributions.""" import math +import operator import os.path as op import matplotlib as mpl import matplotlib.pyplot as plt import numpy as np +import pandas as pd import seaborn as sns from matplotlib.backends.backend_pdf import FigureCanvasPdf as FigureCanvas from matplotlib.colorbar import ColorbarBase @@ -626,8 +628,6 @@ def confoundplot( cutoff=None, ylims=None, ): - import seaborn as sns - # Define TR and number of frames notr = False if tr is None: @@ -856,8 +856,6 @@ def confounds_correlation_plot( output_file: :obj:`str` The file where the figure is saved. """ - import pandas as pd - import seaborn as sns confounds_data = pd.read_table(confounds_file) @@ -932,3 +930,277 @@ def confounds_correlation_plot( figure = None return output_file return [ax0, ax1], gs + + +def _plot_density(x, y, df, group_name, palette, orient): + ax = sns.violinplot( + x=x, + y=y, + data=df, + hue=group_name, + dodge=False, + palette=palette, + density_norm="width", + inner=None, + orient=orient, + ) + + # Cut half of the violins + for violin in ax.collections: + bbox = violin.get_paths()[0].get_extents() + x0, y0, width, height = bbox.bounds + width_denom = 2 + height_denom = 1 + if orient == "h": + width_denom = 1 + height_denom = 2 + violin.set_clip_path( + plt.Rectangle( + (x0, y0), width / width_denom, height / height_denom, transform=ax.transData + ) + ) + + return ax + + +def _jitter_data_points(old_len_collections, orient, width, ax): + offset = np.array([width, 0]) + if orient == "h": + offset = np.array([0, width]) + for dots in ax.collections[old_len_collections:]: + dots.set_offsets(dots.get_offsets() + offset) + + +def _plot_nans(df, x, y, color, orient, ax): + df_nans = df[df.isna().any(axis=1)] + sns.stripplot( + x=x, + y=y, + data=df_nans, + color=color, + orient=orient, + ax=ax, + ) + + +def _plot_out_of_range( + df, + x, + feature, + orient, + limit_offset, + limit_value, + limit_color, + limit_name, + color_vble_name, + _op, + ax, +): + if limit_color is None: + raise ValueError( + f"``{color_vble_name}`` must be provided if ``{limit_name}`` is provided." + ) + if limit_offset is None: + raise ValueError(f"``limit_offset`` must be provided if ``{limit_name}`` is provided.") + if _op == operator.gt: + arithm = operator.add + elif _op == operator.lt: + arithm = operator.sub + else: + raise ValueError(f"``{_op}`` must be either ``gt`` or ``lt``.") + + df_overflow = df[_op(df[feature], limit_value)] + sns.stripplot( + x=x, + y=arithm(limit_value, limit_offset), + data=df_overflow, + color=limit_color, + orient=orient, + ax=ax, + ) + + +def plot_raincloud( + data_file, + group_name, + feature, + palette="Set2", + orient="v", + density=True, + upper_limit_value=None, + upper_limit_color="gray", + lower_limit_value=None, + lower_limit_color="gray", + limit_offset=None, + mark_nans=True, + nans_value=None, + nans_color="black", + figure=None, + output_file=None, +): + """ + Generate a raincloud plot with the data points corresponding to the + ``feature`` value contained in the data file. If ``upper_limit_value`` or + ``lower_limit_value`` is provided, the values outside that range are + clipped. Thus, a large density around those values, together with the values + plot with the distinctive ``upper_limit_color`` and ``lower_limit_color`` + styles may be indicative of unexpected values in the data. Similarly, NaN + values, if present, will be marked with the distinctive ``nans_color`` + style, and may again be indicative of unexpected values in the data. + + Parameters + ---------- + data_file : :obj:`str` + File containing the data of interest. + figure : :obj:`matplotlib.pyplot.figure` or None + Existing figure on which to plot. + group_name : :obj:`str` + The group name of interest to be plot. + feature : :obj:`str` + The feature of interest to be plot. + palette : :obj:`str`, optional + Color palette name provided to :func:`sns.stripplot`. + orient : :obj:`str`, optional + Plot orientation (``v`` or ``h``). + density : :obj:`bool`, optional + ``True`` to plot the density of the data points. + upper_limit_value : :obj:`float`, optional + Upper limit value over which any value in the data will be styled with a + different style. + upper_limit_color : :obj:`str`, optional + Color name to represent values over ``upper_limit_value``. + lower_limit_value : :obj:`float`, optional + Lower limit value under which any value in the data will be styled with + a different style. + lower_limit_color : :obj:`str`, optional + Color name to represent values under ``lower_limit_value``. + limit_offset : :obj:`float`, optional + Offset to plot the values over/under the upper/lower limit values. + mark_nans : :obj:`bool`, optional + ``True`` to plot NaNs as dots. ``nans_values`` must be provided if True. + nans_value : :obj:`float`, optional + Value to use for NaN values. + nans_color : :obj:`str`, optional + Color name to represent NaN values. + output_file : :obj:`str` or :obj:`None` + Path where the output figure should be saved. If this is not defined, + then the plotting axes will be returned instead of the saved figure + path. + + Returns + ------- + axes and gridspec + Plotting axes and gridspec. Returned only if ``output_file`` is ``None``. + output_file : :obj:`str` + The file where the figure is saved. + """ + + df = pd.read_csv(data_file, sep=r"[\t\s]+", engine="python") + + df_clip = df.copy(deep=True) + df_clip[feature] = df[feature].clip(lower=lower_limit_value, upper=upper_limit_value) + + if figure is None: + plt.figure(figsize=(7, 5)) + + gs = GridSpec(1, 1) + ax = plt.subplot(gs[0, 0]) + + sns.set(style="white", font_scale=2) + + x = feature + y = group_name + # Swap x/y if the requested orientation is vertical + if orient == "v": + x = group_name + y = feature + + # Plot the density + if density: + ax = _plot_density(x, y, df_clip, group_name, palette, orient) + + # Add boxplots + width = 0.15 + sns.boxplot( + x=x, + y=y, + data=df_clip, + color="black", + width=width, + zorder=10, + showcaps=True, + boxprops={"facecolor": "none", "zorder": 10}, + showfliers=True, + whiskerprops={"linewidth": 2, "zorder": 10}, + saturation=1, + orient=orient, + ax=ax, + ) + + old_len_collections = len(ax.collections) + + # Plot the data points as dots + sns.stripplot( + x=x, + y=y, + hue=group_name, + data=df_clip, + palette=palette, + edgecolor="white", + size=3, + jitter=0.1, + zorder=0, + orient=orient, + ax=ax, + ) + + # Offset the dots that would be otherwise shadowed by the violins + if density: + _jitter_data_points(old_len_collections, orient, width, ax) + + # Draw nans if any + if mark_nans: + if nans_value is None: + raise ValueError("``nans_value`` must be provided if ``mark_nans`` is True.") + _plot_nans(df, x, nans_value, nans_color, orient, ax) + + # If upper/lower limits are provided, draw the points with a different color + if upper_limit_value is not None: + _plot_out_of_range( + df, + x, + feature, + orient, + limit_offset, + upper_limit_value, + upper_limit_color, + "upper_limit_value", + "upper_limit_color", + operator.gt, + ax, + ) + + if lower_limit_value is not None: + _plot_out_of_range( + df, + x, + feature, + orient, + limit_offset, + lower_limit_value, + lower_limit_color, + "lower_limit_value", + "lower_limit_color", + operator.lt, + ax, + ) + + if output_file is not None: + figure = plt.gcf() + plt.tight_layout() + figure.savefig(output_file, bbox_inches="tight") + plt.close(figure) + figure = None + return output_file + + return ax, gs diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 6e43fea4..32a32e08 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -27,7 +27,8 @@ import pytest from matplotlib import pyplot as plt -from nireports.reportlets.modality.dwi import plot_dwi, plot_gradients +from nireports.reportlets.modality.dwi import plot_dwi, plot_gradients, plot_tissue_values +from nireports.tests.utils import _generate_raincloud_random_data def test_plot_dwi(tmp_path, testdata_path, outdir): @@ -69,3 +70,42 @@ def test_plot_gradients(tmp_path, testdata_path, dwi_btable, outdir): if outdir is not None: plt.savefig(outdir / f"{dwi_btable}.svg", bbox_inches="tight") + + +def test_plot_tissue_values(tmp_path): + features_label = "fa" + group_label = "tissue" + group_names = ["CSF", "GM", "WM"] + min_val_csf = 0.0 + max_val_csf = 0.2 + min_max_csf = (min_val_csf, max_val_csf) + min_val_gm = 0.0 + max_val_gm = 0.6 + min_max_gm = (min_val_gm, max_val_gm) + min_val_wm = 0.3 + max_val_wm = 1.0 + min_max_wm = (min_val_wm, max_val_wm) + min_max = [min_max_csf, min_max_gm, min_max_wm] + n_grp_samples = 250 + data_file = tmp_path / "tissue_fa.tsv" + + _generate_raincloud_random_data( + min_max, n_grp_samples, features_label, group_label, group_names, data_file + ) + + palette = "Set2" + orient = "v" + density = True + output_file = tmp_path / "tissue_fa.png" + mark_nans = False + + plot_tissue_values( + data_file, + group_label, + features_label, + palette=palette, + orient=orient, + density=density, + mark_nans=mark_nans, + output_file=output_file, + ) diff --git a/nireports/tests/test_interfaces.py b/nireports/tests/test_interfaces.py index d361dd6b..d027c0e1 100644 --- a/nireports/tests/test_interfaces.py +++ b/nireports/tests/test_interfaces.py @@ -27,7 +27,12 @@ import pytest -from nireports.interfaces.nuisance import CompCorVariancePlot, ConfoundsCorrelationPlot +from nireports.interfaces.nuisance import ( + CompCorVariancePlot, + ConfoundsCorrelationPlot, + RaincloudPlot, +) +from nireports.tests.utils import _generate_raincloud_random_data def _smoke_test_report(report_interface, artifact_name): @@ -56,3 +61,38 @@ def test_ConfoundsCorrelationPlot(datadir, ignore_initial_volumes): ignore_initial_volumes=ignore_initial_volumes, ) _smoke_test_report(cc_rpt, f"confounds_correlation_{ignore_initial_volumes}.svg") + + +@pytest.mark.parametrize("orient", ["h", "v"]) +@pytest.mark.parametrize("density", (True, False)) +def test_RaincloudPlot(orient, density, tmp_path): + """Raincloud plot report test""" + features_label = "value" + group_label = "group" + group_names = ["group1", "group2"] + min_val_grp1 = 0.3 + max_val_grp1 = 1.0 + min_max_group1 = (min_val_grp1, max_val_grp1) + min_val_grp2 = 0.0 + max_val_grp2 = 0.6 + min_max_group2 = (min_val_grp2, max_val_grp2) + min_max = [min_max_group1, min_max_group2] + n_grp_samples = 250 + data_file = tmp_path / "data.tsv" + + _generate_raincloud_random_data( + min_max, n_grp_samples, features_label, group_label, group_names, data_file + ) + + palette = "Set2" + mark_nans = False + rc_rpt = RaincloudPlot( + data_file=data_file, + group_name=group_label, + feature=features_label, + palette=palette, + orient=orient, + density=density, + mark_nans=mark_nans, + ) + _smoke_test_report(rc_rpt, f"raincloud_orient-{orient}_density-{density}.svg") diff --git a/nireports/tests/test_reportlets.py b/nireports/tests/test_reportlets.py index 671334e0..3f6cd1ee 100644 --- a/nireports/tests/test_reportlets.py +++ b/nireports/tests/test_reportlets.py @@ -35,9 +35,10 @@ from nireports.reportlets.modality.func import fMRIPlot from nireports.reportlets.mosaic import plot_mosaic -from nireports.reportlets.nuisance import plot_carpet +from nireports.reportlets.nuisance import plot_carpet, plot_raincloud from nireports.reportlets.surface import cifti_surfaces_plot from nireports.reportlets.xca import compcor_variance_plot, plot_melodic_components +from nireports.tests.utils import _generate_raincloud_random_data from nireports.tools.timeseries import cifti_timeseries as _cifti_timeseries from nireports.tools.timeseries import get_tr as _get_tr from nireports.tools.timeseries import nifti_timeseries as _nifti_timeseries @@ -368,3 +369,105 @@ def test_mriqc_plot_mosaic_2(tmp_path, testdata_path, outdir): maxrows=12, annotate=True, ) + + +@pytest.mark.parametrize("orient", ["h", "v"]) +@pytest.mark.parametrize("density", (True, False)) +def test_plot_raincloud(orient, density, tmp_path): + features_label = "value" + group_label = "group" + group_names = ["group1", "group2"] + min_val_grp1 = 0.3 + max_val_grp1 = 1.0 + min_max_group1 = (min_val_grp1, max_val_grp1) + min_val_grp2 = 0.0 + max_val_grp2 = 0.6 + min_max_group2 = (min_val_grp2, max_val_grp2) + min_max = [min_max_group1, min_max_group2] + n_grp_samples = 250 + data_file = tmp_path / "data.tsv" + + _generate_raincloud_random_data( + min_max, n_grp_samples, features_label, group_label, group_names, data_file + ) + + palette = "Set2" + mark_nans = False + output_file = tmp_path / f"raincloud_reg_orient-{orient}_density-{density}.png" + + plot_raincloud( + data_file, + group_label, + features_label, + palette=palette, + orient=orient, + density=density, + mark_nans=mark_nans, + output_file=output_file, + ) + + group_nans = [50, 0] + + _generate_raincloud_random_data( + min_max, + n_grp_samples, + features_label, + group_label, + group_names, + data_file, + group_nans=group_nans, + ) + + mark_nans = True + nans_value = 2.0 + output_file = tmp_path / f"raincloud_nans_orient-{orient}_density-{density}.png" + + plot_raincloud( + data_file, + group_label, + features_label, + palette=palette, + orient=orient, + density=density, + mark_nans=mark_nans, + nans_value=nans_value, + output_file=output_file, + ) + + min_val_grp1 = 0.3 + max_val_grp1 = 8.0 + min_max_group1 = (min_val_grp1, max_val_grp1) + min_val_grp2 = -0.2 + max_val_grp2 = 0.6 + min_max_group2 = (min_val_grp2, max_val_grp2) + min_max = [min_max_group1, min_max_group2] + + _generate_raincloud_random_data( + min_max, + n_grp_samples, + features_label, + group_label, + group_names, + data_file, + group_nans=group_nans, + ) + + upper_limit_value = 1.0 + lower_limit_value = 0.0 + limit_offset = 0.5 + output_file = tmp_path / f"raincloud_nans_limits_orient-{orient}_density-{density}.png" + + plot_raincloud( + data_file, + group_label, + features_label, + palette=palette, + orient=orient, + density=density, + upper_limit_value=upper_limit_value, + lower_limit_value=lower_limit_value, + limit_offset=limit_offset, + mark_nans=mark_nans, + nans_value=nans_value, + output_file=output_file, + ) diff --git a/nireports/tests/utils.py b/nireports/tests/utils.py new file mode 100644 index 00000000..4bcd61d7 --- /dev/null +++ b/nireports/tests/utils.py @@ -0,0 +1,37 @@ +import numpy as np +import pandas as pd + + +def _generate_raincloud_random_data( + min_max, + n_grp_samples, + features_label, + group_label, + group_names, + data_file, + group_nans=None, +): + rng = np.random.default_rng(1234) + + if group_nans is None: + group_nans = [None] * len(min_max) + + # Create some random data in the [min_val, max_val) half-open interval + values = np.array([]) + names = [] + for group_min_max, name, nans in zip(min_max, group_names, group_nans): + min_val = group_min_max[0] + max_val = group_min_max[1] + range_size = max_val - min_val + _values = rng.random(n_grp_samples) * range_size + min_val + + values = np.concatenate((values, _values), axis=0) + names.extend([name] * n_grp_samples) + + if nans: + values = np.concatenate((values, [np.nan] * nans), axis=0) + names.extend([name] * nans) + + df = pd.DataFrame(np.vstack([values, names]).T, columns=[features_label, group_label]) + + df.to_csv(data_file, sep="\t") From 593a29fa3094b3b9af70ac3933cea1ba412ccc17 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 12:26:02 -0400 Subject: [PATCH 05/30] Adds higher level carpot_plot function --- nireports/reportlets/modality/dwi.py | 95 ++++++++++++++++++++++++++++ nireports/tests/test_dwi.py | 21 +++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 117b16f5..6f7b19d7 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -28,6 +28,7 @@ from matplotlib import pyplot as plt from mpl_toolkits.mplot3d import art3d from nilearn.plotting import plot_anat +from nireports.reportlets.nuisance import plot_carpet as nw_plot_carpet def plot_dwi(dataobj, affine, gradient=None, **kwargs): @@ -405,3 +406,97 @@ def plot_gradients( plt.suptitle(title) return ax + + +def plot_carpet( + nii, + gtab, + segmentation=None, + sort_by_bval=False, + output_file=None, + segment_labels=None, + detrend=False, +): + """ + Return carpet plot using niworkflows carpet_plot + + Parameters + ---------- + nii : Nifti1Image + DW imaging data + gtab : :obj:`GradientTable` + DW imaging data gradient data + segmentation : Nifti1Image + Boolean or segmentation mask of DW imaging data + sort_by_bval : :obj:`bool` + Flag to reorder time points by bvalue + output_file : :obj:`string` + Path to save the plot + segment_labels : :obj:`dict` + Dictionary of segment labels + e.g. {'Cerebral_White_Matter': [2, 41], + 'Cerebral_Cortex': [3, 42], + 'Ventricle': [4, 14, 15, 43, 72]} + detrend : :obj:`bool` + niworkflows plot_carpet detrend flag + + Returns + --------- + matplotlib GridSpec object + """ + segments = None + + nii_data = nii.get_fdata() + + b0_data = nii_data[..., gtab.b0s_mask] + dw_data = nii_data[..., ~gtab.b0s_mask] + + bzero = np.mean(b0_data, -1) + + nii_data_div_b0 = dw_data / bzero[..., np.newaxis] + + sort_inds = ( + np.argsort(gtab.bvals[~gtab.b0s_mask] if sort_by_bval + else np.arange(len(gtab.bvals[~gtab.b0s_mask]))) + ) + nii_data_div_b0 = nii_data_div_b0[..., sort_inds] + + # Reshape + nii_data_reshaped = nii_data_div_b0.reshape(-1, nii_data_div_b0.shape[-1]) + + if segmentation is not None: + segmentation_data = np.asanyarray(segmentation.dataobj, dtype=np.int16) + + # Apply mask + segmentation_reshaped = segmentation_data.reshape(-1) + nii_data_masked = nii_data_reshaped[segmentation_reshaped > 0, :] + segmentation_masked = segmentation_reshaped[segmentation_reshaped > 0] + + if segment_labels is not None: + segments = dict() + labels = list(segment_labels.keys()) + for label in labels: + indices = np.array([], dtype=int) + for ii in segment_labels[label]: + indices = np.concatenate( + [indices, np.where(segmentation_masked == ii)[0]] + ) + segments[label] = indices + + else: + nii_data_masked = nii_data_reshaped + + bad_row_ind = np.where(~np.isfinite(nii_data_masked))[0] + + good_row_ind = np.ones(nii_data_masked.shape[0], dtype=bool) + good_row_ind[bad_row_ind] = False + + nii_data_masked = nii_data_masked[good_row_ind, :] + + # Plot + return nw_plot_carpet( + nii_data_masked, + detrend=detrend, + segments=segments, + output_file=output_file + ) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 6e43fea4..3b3bd154 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -27,7 +27,10 @@ import pytest from matplotlib import pyplot as plt -from nireports.reportlets.modality.dwi import plot_dwi, plot_gradients +import os.path as op +import dipy.core.gradients as dpg +from nireports.reportlets.modality.dwi \ + import plot_dwi, plot_gradients, plot_carpet def test_plot_dwi(tmp_path, testdata_path, outdir): @@ -69,3 +72,19 @@ def test_plot_gradients(tmp_path, testdata_path, dwi_btable, outdir): if outdir is not None: plt.savefig(outdir / f"{dwi_btable}.svg", bbox_inches="tight") + + +def test_plot_carpet(tmp_path, testdata_path, outdir): + """Check the carpet plot""" + + testdata_name = "ds000114_sub-01_ses-test_desc-trunc_dwi" + + nii_path = testdata_path / f'{testdata_name}.nii.gz' + bvec_path = testdata_path / f'{testdata_name}.bvec' + bval_path = testdata_path / f'{testdata_name}.bval' + + nii = nb.load(nii_path) + gtab = dpg.gradient_table(bval_path, bvec_path) + image_path = outdir / f'{testdata_name}_carpet.png' + + plot_carpet(nii, gtab, output_file=image_path) From 1c2aaff1e00e3c7d09fd207238a5b78f4093ccb2 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 13:49:39 -0400 Subject: [PATCH 06/30] Adds default if outdir None --- nireports/tests/test_dwi.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 3b3bd154..3623301f 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -85,6 +85,10 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): nii = nb.load(nii_path) gtab = dpg.gradient_table(bval_path, bvec_path) - image_path = outdir / f'{testdata_name}_carpet.png' + + image_path = None + + if outdir is not None: + image_path = outdir / f'{testdata_name}_carpet.svg' plot_carpet(nii, gtab, output_file=image_path) From 7783b9c5c7b8761635ecd6b32985023570c81dfc Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 14:10:57 -0400 Subject: [PATCH 07/30] Removes dipy dependency --- nireports/reportlets/modality/dwi.py | 14 +++++++------- nireports/tests/test_dwi.py | 11 +++-------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 6f7b19d7..1a0c63bb 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -410,7 +410,7 @@ def plot_gradients( def plot_carpet( nii, - gtab, + bvals, segmentation=None, sort_by_bval=False, output_file=None, @@ -424,8 +424,8 @@ def plot_carpet( ---------- nii : Nifti1Image DW imaging data - gtab : :obj:`GradientTable` - DW imaging data gradient data + bvals : :obj:`numpy.ndarray` + Rounded bvals segmentation : Nifti1Image Boolean or segmentation mask of DW imaging data sort_by_bval : :obj:`bool` @@ -448,16 +448,16 @@ def plot_carpet( nii_data = nii.get_fdata() - b0_data = nii_data[..., gtab.b0s_mask] - dw_data = nii_data[..., ~gtab.b0s_mask] + b0_data = nii_data[..., bvals == 0] + dw_data = nii_data[..., bvals > 0] bzero = np.mean(b0_data, -1) nii_data_div_b0 = dw_data / bzero[..., np.newaxis] sort_inds = ( - np.argsort(gtab.bvals[~gtab.b0s_mask] if sort_by_bval - else np.arange(len(gtab.bvals[~gtab.b0s_mask]))) + np.argsort(bvals[bvals > 0] if sort_by_bval + else np.arange(len(bvals[bvals > 0]))) ) nii_data_div_b0 = nii_data_div_b0[..., sort_inds] diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 3623301f..4cbebe55 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -28,7 +28,6 @@ from matplotlib import pyplot as plt import os.path as op -import dipy.core.gradients as dpg from nireports.reportlets.modality.dwi \ import plot_dwi, plot_gradients, plot_carpet @@ -79,16 +78,12 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): testdata_name = "ds000114_sub-01_ses-test_desc-trunc_dwi" - nii_path = testdata_path / f'{testdata_name}.nii.gz' - bvec_path = testdata_path / f'{testdata_name}.bvec' - bval_path = testdata_path / f'{testdata_name}.bval' - - nii = nb.load(nii_path) - gtab = dpg.gradient_table(bval_path, bvec_path) + nii = nb.load(testdata_path / f'{testdata_name}.nii.gz') + bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') image_path = None if outdir is not None: image_path = outdir / f'{testdata_name}_carpet.svg' - plot_carpet(nii, gtab, output_file=image_path) + plot_carpet(nii, bvals, output_file=image_path) From 51b7f80808a9135d0981352e664d401615073797 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 14:55:10 -0400 Subject: [PATCH 08/30] Adds segmentation to test and makes bval optional --- nireports/reportlets/modality/dwi.py | 36 +++++++++++++--------------- nireports/tests/test_dwi.py | 11 ++++++++- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 1a0c63bb..bc21a6dd 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -410,7 +410,7 @@ def plot_gradients( def plot_carpet( nii, - bvals, + bvals=None, segmentation=None, sort_by_bval=False, output_file=None, @@ -448,28 +448,29 @@ def plot_carpet( nii_data = nii.get_fdata() - b0_data = nii_data[..., bvals == 0] - dw_data = nii_data[..., bvals > 0] + if bvals is not None: + b0_data = nii_data[..., bvals == 0] + dw_data = nii_data[..., bvals > 0] - bzero = np.mean(b0_data, -1) + bzero = np.mean(b0_data, -1) - nii_data_div_b0 = dw_data / bzero[..., np.newaxis] + nii_data = dw_data / bzero[..., np.newaxis] - sort_inds = ( - np.argsort(bvals[bvals > 0] if sort_by_bval - else np.arange(len(bvals[bvals > 0]))) - ) - nii_data_div_b0 = nii_data_div_b0[..., sort_inds] + sort_inds = ( + np.argsort(bvals[bvals > 0] if sort_by_bval + else np.arange(len(bvals[bvals > 0]))) + ) + nii_data = nii_data[..., sort_inds] # Reshape - nii_data_reshaped = nii_data_div_b0.reshape(-1, nii_data_div_b0.shape[-1]) + nii_data = nii_data.reshape(-1, nii_data.shape[-1]) if segmentation is not None: segmentation_data = np.asanyarray(segmentation.dataobj, dtype=np.int16) # Apply mask segmentation_reshaped = segmentation_data.reshape(-1) - nii_data_masked = nii_data_reshaped[segmentation_reshaped > 0, :] + nii_data = nii_data[segmentation_reshaped > 0, :] segmentation_masked = segmentation_reshaped[segmentation_reshaped > 0] if segment_labels is not None: @@ -483,19 +484,16 @@ def plot_carpet( ) segments[label] = indices - else: - nii_data_masked = nii_data_reshaped - - bad_row_ind = np.where(~np.isfinite(nii_data_masked))[0] + bad_row_ind = np.where(~np.isfinite(nii_data))[0] - good_row_ind = np.ones(nii_data_masked.shape[0], dtype=bool) + good_row_ind = np.ones(nii_data.shape[0], dtype=bool) good_row_ind[bad_row_ind] = False - nii_data_masked = nii_data_masked[good_row_ind, :] + nii_data = nii_data[good_row_ind, :] # Plot return nw_plot_carpet( - nii_data_masked, + nii_data, detrend=detrend, segments=segments, output_file=output_file diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 4cbebe55..17e11b0e 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -81,9 +81,18 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): nii = nb.load(testdata_path / f'{testdata_name}.nii.gz') bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') + nii_data = nii.get_fdata() + segmentation = nii_data > 3000 + segment_labels = {"<3000": [0], ">3000": [1]} + image_path = None if outdir is not None: image_path = outdir / f'{testdata_name}_carpet.svg' - plot_carpet(nii, bvals, output_file=image_path) + plot_carpet(nii, + bvals=bvals, + segmentation=segmentation, + segment_labels=segment_labels, + output_file=image_path + ) From 2a135cc1b6a5a0c8b87bca11d90e1a687412564f Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 15:10:28 -0400 Subject: [PATCH 09/30] Updates test segmentation --- nireports/tests/test_dwi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 17e11b0e..fb14844d 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -82,8 +82,8 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') nii_data = nii.get_fdata() - segmentation = nii_data > 3000 - segment_labels = {"<3000": [0], ">3000": [1]} + segmentation = round(3*np.rand(nii_data.shape)) + segment_labels = {"0": [0], "1": [1], "2": [2], "3": [3]} image_path = None From f22f6c15204987db79786e3c0d7bb60cbe8bdae0 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 15:26:00 -0400 Subject: [PATCH 10/30] Random segmentation --- nireports/tests/test_dwi.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index fb14844d..71a215ba 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -82,7 +82,10 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') nii_data = nii.get_fdata() - segmentation = round(3*np.rand(nii_data.shape)) + segmentation = np.round(3*np.random.rand(nii_data.shape[0], + nii_data.shape[1], + nii_data.shape[2], + nii_data.shape[3])) segment_labels = {"0": [0], "1": [1], "2": [2], "3": [3]} image_path = None From 2a56cf9b27de51c66f2fe026631cc63f4d008dc7 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 15:35:05 -0400 Subject: [PATCH 11/30] Changes segmentation to np array --- nireports/reportlets/modality/dwi.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index bc21a6dd..1a1f5fbe 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -426,8 +426,9 @@ def plot_carpet( DW imaging data bvals : :obj:`numpy.ndarray` Rounded bvals - segmentation : Nifti1Image + segmentation : :obj:`numpy.ndarray` Boolean or segmentation mask of DW imaging data + e.g. np.asanyarray(Nifti1Image.dataobj, dtype=np.int16) sort_by_bval : :obj:`bool` Flag to reorder time points by bvalue output_file : :obj:`string` @@ -466,10 +467,8 @@ def plot_carpet( nii_data = nii_data.reshape(-1, nii_data.shape[-1]) if segmentation is not None: - segmentation_data = np.asanyarray(segmentation.dataobj, dtype=np.int16) - # Apply mask - segmentation_reshaped = segmentation_data.reshape(-1) + segmentation_reshaped = segmentation.reshape(-1) nii_data = nii_data[segmentation_reshaped > 0, :] segmentation_masked = segmentation_reshaped[segmentation_reshaped > 0] From 2faac2c90bfa0ca4bd9e313b8b32c263f341171f Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Wed, 15 May 2024 15:55:14 -0400 Subject: [PATCH 12/30] Segmentation fixes --- nireports/reportlets/modality/dwi.py | 1 + nireports/tests/test_dwi.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 1a1f5fbe..a2014133 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -428,6 +428,7 @@ def plot_carpet( Rounded bvals segmentation : :obj:`numpy.ndarray` Boolean or segmentation mask of DW imaging data + Data corresponding to 0 values will be dropped e.g. np.asanyarray(Nifti1Image.dataobj, dtype=np.int16) sort_by_bval : :obj:`bool` Flag to reorder time points by bvalue diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 71a215ba..9d8f8bb6 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -84,8 +84,7 @@ def test_plot_carpet(tmp_path, testdata_path, outdir): nii_data = nii.get_fdata() segmentation = np.round(3*np.random.rand(nii_data.shape[0], nii_data.shape[1], - nii_data.shape[2], - nii_data.shape[3])) + nii_data.shape[2])) segment_labels = {"0": [0], "1": [1], "2": [2], "3": [3]} image_path = None From 41f1daf0d06a64c2727da441eb1900081974626d Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Thu, 16 May 2024 10:54:29 -0400 Subject: [PATCH 13/30] Adds get_segment_labels() --- nireports/reportlets/modality/dwi.py | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index a2014133..25cda5c7 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -498,3 +498,52 @@ def plot_carpet( segments=segments, output_file=output_file ) + + +def get_segment_labels( + filepath, + keywords, + delimiter=" ", + index_position=0, + label_position=1 +): + """ + Return segment labels for plot_carpet function + Parameters + ---------- + filepath : :obj:`string` + Path to segment label text file, such as freesurfer label file + keywords : list of :obj:`string` + List of label keywords. + All labels containing the keyword will be grouped together. + e.g. ["Cerebral_White_Matter", "Cerebral_Cortex", "Ventricle"] + delimiter : :obj:`string` + Delimiter between label index and label string in label file + (' ' for freesurfer label file) + index_position : :obj:`int` + Position of label index in label file + (0 for freesurfer label file) + label_position : :obj:`int` + Position of label string in label file + (1 for freesurfer label file) + Returns + --------- + dict + e.g. {'Cerebral_White_Matter': [2, 41], + 'Cerebral_Cortex': [3, 42], + 'Ventricle': [4, 14, 15, 43, 72]} + """ + segment_labels = {} + + with open(filepath, "r") as f: + labels = f.read() + + labels_s = [label.split(delimiter) for label in labels.split("\n") + if label != ""] + + for keyword in keywords: + ind = [int(i[index_position]) for i in labels_s + if keyword in i[label_position]] + segment_labels[keyword] = ind + + return segment_labels From 1465cd5a905c2e7017976cb50043795c752c0bf2 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Thu, 16 May 2024 15:51:09 -0400 Subject: [PATCH 14/30] Breaks higher level function into preprocessing functions --- nireports/reportlets/modality/dwi.py | 160 +++++++++++------- .../aseg.auto_noCCseg.label_intensities.txt | 47 +++++ nireports/tests/test_dwi.py | 41 +++-- 3 files changed, 166 insertions(+), 82 deletions(-) create mode 100644 nireports/tests/data/aseg.auto_noCCseg.label_intensities.txt diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 25cda5c7..8315e101 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -28,7 +28,6 @@ from matplotlib import pyplot as plt from mpl_toolkits.mplot3d import art3d from nilearn.plotting import plot_anat -from nireports.reportlets.nuisance import plot_carpet as nw_plot_carpet def plot_dwi(dataobj, affine, gradient=None, **kwargs): @@ -408,96 +407,128 @@ def plot_gradients( return ax -def plot_carpet( +def nii_to_carpetplot_data( nii, bvals=None, - segmentation=None, + divide_by_b0=True, + drop_b0=True, sort_by_bval=False, - output_file=None, - segment_labels=None, - detrend=False, + mask_nii=None, + segment_labels=None ): """ - Return carpet plot using niworkflows carpet_plot + Convert nii to data matrix for carpet plot Parameters ---------- nii : Nifti1Image DW imaging data - bvals : :obj:`numpy.ndarray` + bvals : :obj:`numpy.ndarray`, optional Rounded bvals - segmentation : :obj:`numpy.ndarray` - Boolean or segmentation mask of DW imaging data - Data corresponding to 0 values will be dropped - e.g. np.asanyarray(Nifti1Image.dataobj, dtype=np.int16) - sort_by_bval : :obj:`bool` + divide_by_b0 : :obj:`bool`, optional + Divide data by mean b0 + drop_b0 : :obj:`bool`, optional + Flag to drop b0 data + sort_by_bval : :obj:`bool`, optional Flag to reorder time points by bvalue - output_file : :obj:`string` - Path to save the plot - segment_labels : :obj:`dict` + mask_nii : Nifti1Image, optional + Boolean or segmentation mask of DW imaging data + segment_labels : :obj:`dict`, optional Dictionary of segment labels e.g. {'Cerebral_White_Matter': [2, 41], 'Cerebral_Cortex': [3, 42], 'Ventricle': [4, 14, 15, 43, 72]} - detrend : :obj:`bool` - niworkflows plot_carpet detrend flag Returns --------- - matplotlib GridSpec object + data : N x T :obj:`numpy.array` + The functional data to be plotted + (*N* sampling locations by *T* timepoints). + segments: :obj:`dict` + A mapping between segment labels (e.g., `"Left Cortex"`) + and list of indexes in the data array. """ - segments = None nii_data = nii.get_fdata() - if bvals is not None: - b0_data = nii_data[..., bvals == 0] - dw_data = nii_data[..., bvals > 0] + if mask_nii is None: + mask_data = np.ones(nii_data.shape[0:2]) + else: + mask_data = np.asanyarray(mask_nii.dataobj, dtype=np.int16) - bzero = np.mean(b0_data, -1) + if bvals is not None: + if divide_by_b0: + b0_data = nii_data[..., bvals == 0] + bzero = np.mean(b0_data, -1) + nii_data = nii_data / bzero[..., np.newaxis] - nii_data = dw_data / bzero[..., np.newaxis] + if drop_b0: + nii_data = nii_data[..., bvals > 0] + bvals = bvals[bvals > 0] - sort_inds = ( - np.argsort(bvals[bvals > 0] if sort_by_bval - else np.arange(len(bvals[bvals > 0]))) - ) - nii_data = nii_data[..., sort_inds] + if sort_by_bval: + sort_inds = np.argsort(bvals) + nii_data = nii_data[..., sort_inds] # Reshape - nii_data = nii_data.reshape(-1, nii_data.shape[-1]) - - if segmentation is not None: - # Apply mask - segmentation_reshaped = segmentation.reshape(-1) - nii_data = nii_data[segmentation_reshaped > 0, :] - segmentation_masked = segmentation_reshaped[segmentation_reshaped > 0] - - if segment_labels is not None: - segments = dict() - labels = list(segment_labels.keys()) - for label in labels: - indices = np.array([], dtype=int) - for ii in segment_labels[label]: - indices = np.concatenate( - [indices, np.where(segmentation_masked == ii)[0]] - ) - segments[label] = indices - - bad_row_ind = np.where(~np.isfinite(nii_data))[0] - - good_row_ind = np.ones(nii_data.shape[0], dtype=bool) + data = nii_data.reshape(-1, nii_data.shape[-1]) + mask_data = mask_data.reshape(-1) + + # Apply mask + data = data[mask_data > 0, :] + mask_data = mask_data[mask_data > 0] + + # Remove bad rows + bad_row_ind = np.where(~np.isfinite(data))[0] + good_row_ind = np.ones(data.shape[0], dtype=bool) good_row_ind[bad_row_ind] = False - nii_data = nii_data[good_row_ind, :] + data = data[good_row_ind, :] + mask_data = mask_data[good_row_ind] - # Plot - return nw_plot_carpet( - nii_data, - detrend=detrend, - segments=segments, - output_file=output_file - ) + # Get segments dict + if mask_nii is None or segment_labels is None: + segments = None + else: + segments = get_segments(mask_data, segment_labels) + + return data, segments + + +def get_segments( + segment_mask, + segment_labels +): + """ + Return segments dict for plot_carpet function + + Parameters + ---------- + segment_mask : :obj:`numpy.ndarray` + Segmentation mask of DW imaging data + segment_labels : :obj:`dict` + Dictionary of segment labels + e.g. {'Cerebral_White_Matter': [2, 41], + 'Cerebral_Cortex': [3, 42], + 'Ventricle': [4, 14, 15, 43, 72]} + + Returns + --------- + segments: :obj:`dict` + A mapping between segment labels (e.g., `"Left Cortex"`) + and list of indexes in the data array. + """ + segments = dict() + + for label, idx in segment_labels.items(): + indices = np.array([], dtype=int) + for ii in idx: + indices = np.concatenate( + [indices, np.nonzero(segment_mask == ii)] + ) + segments[label] = indices + + return segments def get_segment_labels( @@ -508,7 +539,8 @@ def get_segment_labels( label_position=1 ): """ - Return segment labels for plot_carpet function + Get segment labels from file by keyword for get_segments function + Parameters ---------- filepath : :obj:`string` @@ -517,13 +549,13 @@ def get_segment_labels( List of label keywords. All labels containing the keyword will be grouped together. e.g. ["Cerebral_White_Matter", "Cerebral_Cortex", "Ventricle"] - delimiter : :obj:`string` + delimiter : :obj:`string`, optional Delimiter between label index and label string in label file (' ' for freesurfer label file) - index_position : :obj:`int` + index_position : :obj:`int`, optional Position of label index in label file (0 for freesurfer label file) - label_position : :obj:`int` + label_position : :obj:`int`, optional Position of label string in label file (1 for freesurfer label file) Returns diff --git a/nireports/tests/data/aseg.auto_noCCseg.label_intensities.txt b/nireports/tests/data/aseg.auto_noCCseg.label_intensities.txt new file mode 100644 index 00000000..5c453366 --- /dev/null +++ b/nireports/tests/data/aseg.auto_noCCseg.label_intensities.txt @@ -0,0 +1,47 @@ +1 Left_Cerebral_Exterior 1.64 0.0 0 +2 Left_Cerebral_White_Matter 1.00 0.0 105 +3 Left_Cerebral_Cortex 1.20 0.0 69 +4 Left_Lateral_Ventricle 1.30 0.0 24 +5 Left_Inf_Lat_Vent 1.27 0.0 41 +7 Left_Cerebellum_White_Matter 1.08 0.0 91 +8 Left_Cerebellum_Cortex 1.32 0.0 73 +9 Left_Thalamus 1.26 0.0 103 +10 Left_Thalamus_Proper 1.03 0.0 93 +11 Left_Caudate 1.10 0.0 75 +12 Left_Putamen 1.16 0.0 94 +13 Left_Pallidum 1.05 0.0 101 +14 Third_Ventricle 1.64 0.0 42 +15 Fourth_Ventricle 1.72 0.0 26 +16 Brain_Stem 1.07 0.0 83 +17 Left_Hippocampus 1.27 0.0 72 +18 Left_Amygdala 1.32 0.0 76 +24 CSF 1.64 0.0 60 +26 Left_Accumbens_area 1.10 0.0 68 +28 Left_VentralDC 1.07 0.0 91 +40 Right_Cerebral_Exterior 1.64 0.0 0 +41 Right_Cerebral_White_Matter 1.00 0.0 103 +42 Right_Cerebral_Cortex 1.20 0.0 69 +43 Right_Lateral_Ventricle 1.94 0.0 25 +44 Right_Inf_Lat_Vent 1.29 0.0 30 +46 Right_Cerebellum_White_Matter 1.05 0.0 89 +47 Right_Cerebellum_Cortex 1.28 0.0 70 +48 Right_Thalamus 1.26 0.0 0 +49 Right_Thalamus_Proper 1.05 0.0 85 +50 Right_Caudate 1.24 0.0 81 +51 Right_Putamen 1.16 0.0 88 +52 Right_Pallidum 1.03 0.0 99 +53 Right_Hippocampus 1.29 0.0 72 +54 Right_Amygdala 1.29 0.0 72 +58 Right_Accumbens_area 1.24 0.0 80 +60 Right_VentralDC 1.05 0.0 94 +72 Fifth_Ventricle 1.64 0.0 50 +75 1.64 0.0 0 +76 1.64 0.0 0 +77 WM_hypointensities 1.00 0.0 77 +78 Left_WM_hypointensities 1.00 0.0 0 +79 Right_WM_hypointensities 1.00 0.0 0 +80 non_WM_hypointensities 1.00 0.0 44 +81 Left_non_WM_hypointensities 1.00 0.0 0 +82 Right_non_WM_hypointensities 1.00 0.0 0 +186 1.00 0.0 0 +187 1.00 0.0 0 diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 9d8f8bb6..3eb8bb7b 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -27,9 +27,10 @@ import pytest from matplotlib import pyplot as plt -import os.path as op from nireports.reportlets.modality.dwi \ - import plot_dwi, plot_gradients, plot_carpet + import plot_dwi, plot_gradients, nii_to_carpetplot_data, get_segment_labels + +from nireports.reportlets.nuisance import plot_carpet def test_plot_dwi(tmp_path, testdata_path, outdir): @@ -73,28 +74,32 @@ def test_plot_gradients(tmp_path, testdata_path, dwi_btable, outdir): plt.savefig(outdir / f"{dwi_btable}.svg", bbox_inches="tight") -def test_plot_carpet(tmp_path, testdata_path, outdir): - """Check the carpet plot""" +def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): + """Check the nii to carpet plot data function""" testdata_name = "ds000114_sub-01_ses-test_desc-trunc_dwi" nii = nb.load(testdata_path / f'{testdata_name}.nii.gz') bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') - nii_data = nii.get_fdata() - segmentation = np.round(3*np.random.rand(nii_data.shape[0], - nii_data.shape[1], - nii_data.shape[2])) - segment_labels = {"0": [0], "1": [1], "2": [2], "3": [3]} - image_path = None if outdir is not None: - image_path = outdir / f'{testdata_name}_carpet.svg' - - plot_carpet(nii, - bvals=bvals, - segmentation=segmentation, - segment_labels=segment_labels, - output_file=image_path - ) + image_path = outdir / f'{testdata_name}_nii_to_carpet.svg' + + data, segments = nii_to_carpetplot_data(nii, bvals=bvals) + + plot_carpet(data, segments, output_file=image_path) + + +def test_get_segment_labels(tmp_path, testdata_path): + """Check the segment label function""" + + testdata_name = "aseg.auto_noCCseg.label_intensities.txt" + + filepath = testdata_path / testdata_name + keywords = ["Cerebral_White_Matter", "Cerebral_Cortex", "Ventricle"] + + segment_labels = get_segment_labels(filepath, keywords) + + assert segment_labels is not None From 51198f29e7756466d82d21ee78cdf640b2476eb7 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Thu, 16 May 2024 15:59:08 -0400 Subject: [PATCH 15/30] Updates default when no mask --- nireports/reportlets/modality/dwi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 8315e101..63258db5 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -452,7 +452,7 @@ def nii_to_carpetplot_data( nii_data = nii.get_fdata() if mask_nii is None: - mask_data = np.ones(nii_data.shape[0:2]) + mask_data = np.ones(nii_data.shape[0:3]) else: mask_data = np.asanyarray(mask_nii.dataobj, dtype=np.int16) From 3adb4cd8ab7c172f8d9b49642e379523bca18ac8 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Fri, 17 May 2024 10:23:17 -0400 Subject: [PATCH 16/30] Expands test --- nireports/tests/test_dwi.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 3eb8bb7b..df6b2a24 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -82,12 +82,27 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): nii = nb.load(testdata_path / f'{testdata_name}.nii.gz') bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') + mask_data = np.round(82 * np.random.rand(nii.shape[0], + nii.shape[1], + nii.shape[2], + nii.shape[3])) + + mask_nii = nb.Nifti1Image(mask_data, np.eye(4)) + + filepath = testdata_path / "aseg.auto_noCCseg.label_intensities.txt" + keywords = ["Cerebral_White_Matter", "Cerebral_Cortex", "Ventricle"] + + segment_labels = get_segment_labels(filepath, keywords) + image_path = None if outdir is not None: image_path = outdir / f'{testdata_name}_nii_to_carpet.svg' - data, segments = nii_to_carpetplot_data(nii, bvals=bvals) + data, segments = nii_to_carpetplot_data(nii, + bvals=bvals, + mask_nii=mask_nii, + segment_labels=segment_labels) plot_carpet(data, segments, output_file=image_path) From 901bd4deb0548c3cc9a493602c0e69b50e188e7e Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Fri, 17 May 2024 10:32:10 -0400 Subject: [PATCH 17/30] Updates test --- nireports/tests/test_dwi.py | 1 + 1 file changed, 1 insertion(+) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index df6b2a24..19af7560 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -101,6 +101,7 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): data, segments = nii_to_carpetplot_data(nii, bvals=bvals, + drop_b0=False, mask_nii=mask_nii, segment_labels=segment_labels) From aed7b041d98ec821ad24289f2dd892f90b186320 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Fri, 17 May 2024 10:37:28 -0400 Subject: [PATCH 18/30] Updates test segmentation --- nireports/tests/test_dwi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 19af7560..88fa7097 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -84,8 +84,7 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): mask_data = np.round(82 * np.random.rand(nii.shape[0], nii.shape[1], - nii.shape[2], - nii.shape[3])) + nii.shape[2])) mask_nii = nb.Nifti1Image(mask_data, np.eye(4)) @@ -101,7 +100,6 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): data, segments = nii_to_carpetplot_data(nii, bvals=bvals, - drop_b0=False, mask_nii=mask_nii, segment_labels=segment_labels) From c3eee8d4d2748fdaac595b41eb8d839d04d0c238 Mon Sep 17 00:00:00 2001 From: Teresa Gomez <46339554+teresamg@users.noreply.github.com> Date: Fri, 17 May 2024 10:44:45 -0400 Subject: [PATCH 19/30] Updates get_segments() --- nireports/reportlets/modality/dwi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 63258db5..84465e82 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -524,7 +524,7 @@ def get_segments( indices = np.array([], dtype=int) for ii in idx: indices = np.concatenate( - [indices, np.nonzero(segment_mask == ii)] + [indices, np.nonzero(segment_mask == ii)[0]] ) segments[label] = indices From 75cc84eb35489984bac7a3ec868110c5471a2897 Mon Sep 17 00:00:00 2001 From: Oscar Esteban Date: Mon, 1 Jul 2024 15:09:19 +0200 Subject: [PATCH 20/30] sty: revise format --- nireports/reportlets/modality/dwi.py | 27 +++++++-------------------- nireports/tests/test_dwi.py | 26 +++++++++++++------------- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/nireports/reportlets/modality/dwi.py b/nireports/reportlets/modality/dwi.py index 84465e82..1d45702b 100644 --- a/nireports/reportlets/modality/dwi.py +++ b/nireports/reportlets/modality/dwi.py @@ -414,7 +414,7 @@ def nii_to_carpetplot_data( drop_b0=True, sort_by_bval=False, mask_nii=None, - segment_labels=None + segment_labels=None, ): """ Convert nii to data matrix for carpet plot @@ -495,10 +495,7 @@ def nii_to_carpetplot_data( return data, segments -def get_segments( - segment_mask, - segment_labels -): +def get_segments(segment_mask, segment_labels): """ Return segments dict for plot_carpet function @@ -518,26 +515,18 @@ def get_segments( A mapping between segment labels (e.g., `"Left Cortex"`) and list of indexes in the data array. """ - segments = dict() + segments = {} for label, idx in segment_labels.items(): indices = np.array([], dtype=int) for ii in idx: - indices = np.concatenate( - [indices, np.nonzero(segment_mask == ii)[0]] - ) + indices = np.concatenate([indices, np.nonzero(segment_mask == ii)[0]]) segments[label] = indices return segments -def get_segment_labels( - filepath, - keywords, - delimiter=" ", - index_position=0, - label_position=1 -): +def get_segment_labels(filepath, keywords, delimiter=" ", index_position=0, label_position=1): """ Get segment labels from file by keyword for get_segments function @@ -570,12 +559,10 @@ def get_segment_labels( with open(filepath, "r") as f: labels = f.read() - labels_s = [label.split(delimiter) for label in labels.split("\n") - if label != ""] + labels_s = [label.split(delimiter) for label in labels.split("\n") if label != ""] for keyword in keywords: - ind = [int(i[index_position]) for i in labels_s - if keyword in i[label_position]] + ind = [int(i[index_position]) for i in labels_s if keyword in i[label_position]] segment_labels[keyword] = ind return segment_labels diff --git a/nireports/tests/test_dwi.py b/nireports/tests/test_dwi.py index 88fa7097..c90c81ce 100644 --- a/nireports/tests/test_dwi.py +++ b/nireports/tests/test_dwi.py @@ -27,9 +27,12 @@ import pytest from matplotlib import pyplot as plt -from nireports.reportlets.modality.dwi \ - import plot_dwi, plot_gradients, nii_to_carpetplot_data, get_segment_labels - +from nireports.reportlets.modality.dwi import ( + get_segment_labels, + nii_to_carpetplot_data, + plot_dwi, + plot_gradients, +) from nireports.reportlets.nuisance import plot_carpet @@ -79,12 +82,10 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): testdata_name = "ds000114_sub-01_ses-test_desc-trunc_dwi" - nii = nb.load(testdata_path / f'{testdata_name}.nii.gz') - bvals = np.loadtxt(testdata_path / f'{testdata_name}.bval') + nii = nb.load(testdata_path / f"{testdata_name}.nii.gz") + bvals = np.loadtxt(testdata_path / f"{testdata_name}.bval") - mask_data = np.round(82 * np.random.rand(nii.shape[0], - nii.shape[1], - nii.shape[2])) + mask_data = np.round(82 * np.random.rand(nii.shape[0], nii.shape[1], nii.shape[2])) mask_nii = nb.Nifti1Image(mask_data, np.eye(4)) @@ -96,12 +97,11 @@ def test_nii_to_carpetplot_data(tmp_path, testdata_path, outdir): image_path = None if outdir is not None: - image_path = outdir / f'{testdata_name}_nii_to_carpet.svg' + image_path = outdir / f"{testdata_name}_nii_to_carpet.svg" - data, segments = nii_to_carpetplot_data(nii, - bvals=bvals, - mask_nii=mask_nii, - segment_labels=segment_labels) + data, segments = nii_to_carpetplot_data( + nii, bvals=bvals, mask_nii=mask_nii, segment_labels=segment_labels + ) plot_carpet(data, segments, output_file=image_path) From b51d6e52fec31b506561c7af458175a0fa40baf1 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 16 Aug 2024 13:07:51 -0400 Subject: [PATCH 21/30] Update reportlet.py --- nireports/assembler/reportlet.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/nireports/assembler/reportlet.py b/nireports/assembler/reportlet.py index a69c421a..28862363 100644 --- a/nireports/assembler/reportlet.py +++ b/nireports/assembler/reportlet.py @@ -270,6 +270,26 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat name=html_anchor, style="; ".join(f"{k}: {v}" for k, v in style.items()), ) + elif ext in (".png", ".jpg", ".jpeg"): + entities = dict(bidsfile.entities) + if desc_text: + desc_text = desc_text.format(**entities) + + try: + html_anchor = src.relative_to(out_dir) + except ValueError: + html_anchor = src.relative_to(Path(layout.root)) + dst = out_dir / html_anchor + dst.parent.mkdir(parents=True, exist_ok=True) + copyfile(src, dst, copy=True, use_hardlink=True) + + style = {"width": "100%"} if is_static else {} + style.update(config.get("style", {})) + + stylestr = "; ".join(f"{k}: {v}" for k, v in style.items()) + contents = f'' + else: + raise RuntimeError(f"Unsupported file extension: {ext}") if contents: self.components.append((contents, desc_text)) From 662be80c4eec342bc07ba61a4e5cafd0db0efc04 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 16 Aug 2024 13:13:58 -0400 Subject: [PATCH 22/30] Show contents. --- nireports/assembler/report.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index eb858601..f2b337a0 100644 --- a/nireports/assembler/report.py +++ b/nireports/assembler/report.py @@ -220,6 +220,9 @@ class Report: ... ).read_text()) - (REPORT_BASELINE_LENGTH + 51892) 0 + >>> (output_dir / 'nireports' / 'sub-03_task-faketaskwithruns_bold.html').read_text() + "test" + >>> len(( ... output_dir / 'nireports' / 'sub-03_task-faketaskwithruns_bold.html' ... ).read_text()) - RATING_WIDGET_LENGTH From bf10a4694e1dd1572b8b04ab7b273af8a2c9b1f3 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 16 Aug 2024 13:20:08 -0400 Subject: [PATCH 23/30] Update report.py --- nireports/assembler/report.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index f2b337a0..eb858601 100644 --- a/nireports/assembler/report.py +++ b/nireports/assembler/report.py @@ -220,9 +220,6 @@ class Report: ... ).read_text()) - (REPORT_BASELINE_LENGTH + 51892) 0 - >>> (output_dir / 'nireports' / 'sub-03_task-faketaskwithruns_bold.html').read_text() - "test" - >>> len(( ... output_dir / 'nireports' / 'sub-03_task-faketaskwithruns_bold.html' ... ).read_text()) - RATING_WIDGET_LENGTH From eb2a35afe911b1c9dd32d76c8accf5f6bd7d5885 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Fri, 16 Aug 2024 13:29:28 -0400 Subject: [PATCH 24/30] Pin numpy version. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1b41c886..2ddd560a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "nibabel >= 3.0.1", "nilearn >= 0.5.2", "nipype", - "numpy", + "numpy < 2.0.0", "pandas", "pybids", "pyyaml", From 3752a18ae3f3bc69020a78eb1054b39c75c688ff Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 09:53:48 -0400 Subject: [PATCH 25/30] Unpin Sphinx. --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2ddd560a..131b2b11 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,10 +41,10 @@ NiPreps = "https://www.nipreps.org/" [project.optional-dependencies] doc = [ - "furo ~= 2021.10.09", + "furo", "pydot >= 1.2.3", "pydotplus", - "sphinx ~= 4.0", + "sphinx", "sphinxcontrib-apidoc", "sphinxcontrib-napoleon", ] From 7efbcfd97e09c197d91b1a2d599edaf4ecd585a3 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 10:51:01 -0400 Subject: [PATCH 26/30] Use snippet for pngs. --- nireports/assembler/reportlet.py | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/nireports/assembler/reportlet.py b/nireports/assembler/reportlet.py index 28862363..d942efe9 100644 --- a/nireports/assembler/reportlet.py +++ b/nireports/assembler/reportlet.py @@ -31,22 +31,20 @@ from nireports.assembler.misc import dict2html, read_crashfile -SVG_SNIPPET = [ - """\ +IMG_SNIPPET = """\
- -Problem loading figure {name}. If the link below works, please try \ -reloading the report in your browser. +
Get figure file: {name} -""", - """\ +""" +SVG_SNIPPET = """\
- + +Problem loading figure {name}. If the link below works, please try \ +reloading the report in your browser.
Get figure file: {name} -""", -] +""" METADATA_ACCORDION_BLOCK = """\
@@ -266,7 +264,9 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat style = {"width": "100%"} if is_static else {} style.update(config.get("style", {})) - contents = SVG_SNIPPET[is_static].format( + snippet = SVG_SNIPPET if is_static else IMG_SNIPPET + contents = snippet.format( + ext=ext[1:], name=html_anchor, style="; ".join(f"{k}: {v}" for k, v in style.items()), ) @@ -283,11 +283,15 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat dst.parent.mkdir(parents=True, exist_ok=True) copyfile(src, dst, copy=True, use_hardlink=True) - style = {"width": "100%"} if is_static else {} + style = {"width": "100%"} style.update(config.get("style", {})) - stylestr = "; ".join(f"{k}: {v}" for k, v in style.items()) - contents = f'' + snippet = IMG_SNIPPET + contents = snippet.format( + ext=ext[1:], + name=html_anchor, + style="; ".join(f"{k}: {v}" for k, v in style.items()), + ) else: raise RuntimeError(f"Unsupported file extension: {ext}") From 53587594d8251dfd9ba20e076967f179a24d0f78 Mon Sep 17 00:00:00 2001 From: Taylor Salo Date: Mon, 19 Aug 2024 10:58:42 -0400 Subject: [PATCH 27/30] Flip them. --- nireports/assembler/reportlet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/assembler/reportlet.py b/nireports/assembler/reportlet.py index d942efe9..b14acc7f 100644 --- a/nireports/assembler/reportlet.py +++ b/nireports/assembler/reportlet.py @@ -264,7 +264,7 @@ def __init__(self, layout, config=None, out_dir=None, bids_filters=None, metadat style = {"width": "100%"} if is_static else {} style.update(config.get("style", {})) - snippet = SVG_SNIPPET if is_static else IMG_SNIPPET + snippet = IMG_SNIPPET if is_static else SVG_SNIPPET contents = snippet.format( ext=ext[1:], name=html_anchor, From dc54385842c370eedaed5b1b5f0cbd9eb5692899 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 19 Aug 2024 11:15:59 -0400 Subject: [PATCH 28/30] REL: 23.2.2 --- CHANGES.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index 1af0fe94..ab27a29f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,15 @@ +23.2.2 (August 19, 2024) +======================== +Bug-fix release in the 23.2.x series. + +CHANGES +------- + +**Full Changelog**: https://github.com/nipreps/nireports/compare/23.2.1...23.2.2 + +* ENH: Support PNGs and JPGs in reportlets (#126) + + 23.2.1 (May 07, 2024) ===================== Bug-fix release in the 23.2.x series. From c980e130d8e5c7b1470b31e9a97870a94bf22227 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 19 Aug 2024 11:42:40 -0400 Subject: [PATCH 29/30] test: Fix expected length of widget (see 4d43a88) --- nireports/assembler/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nireports/assembler/report.py b/nireports/assembler/report.py index 0555fc6c..b58fcfca 100644 --- a/nireports/assembler/report.py +++ b/nireports/assembler/report.py @@ -96,7 +96,7 @@ class Report: ... dirs_exist_ok=True, ... ) >>> REPORT_BASELINE_LENGTH = 41770 - >>> RATING_WIDGET_LENGTH = 83308 + >>> RATING_WIDGET_LENGTH = 83343 Examples From 449edfa30938951b9cf1f161d52f2b45353673e2 Mon Sep 17 00:00:00 2001 From: Chris Markiewicz Date: Mon, 19 Aug 2024 11:14:22 -0400 Subject: [PATCH 30/30] test: Numpy 2.0 compatibility --- nireports/conftest.py | 8 ++++++++ pyproject.toml | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/nireports/conftest.py b/nireports/conftest.py index c67ff257..4fb25190 100644 --- a/nireports/conftest.py +++ b/nireports/conftest.py @@ -41,6 +41,14 @@ test_workdir = os.getenv("TEST_WORK_DIR") +@pytest.fixture(scope="session", autouse=True) +def legacy_printoptions(): + from packaging.version import Version + + if Version(np.__version__) >= Version("1.22"): + np.set_printoptions(legacy="1.21") + + @pytest.fixture(autouse=True) def expand_namespace(doctest_namespace): doctest_namespace["PY_VERSION"] = version_info diff --git a/pyproject.toml b/pyproject.toml index 297e4bc5..b67d93e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ dependencies = [ "nibabel >= 3.0.1", "nilearn >= 0.5.2", "nipype", - "numpy < 2.0", + "numpy", "pandas", "pybids", "pyyaml", @@ -57,6 +57,7 @@ dev = [ test = [ "coverage", "matplotlib", + "packaging", "pytest", "pytest-cov", "pytest-env",