Repository: nilearn/nilearn

Issue:
Misc issues in Nistats reports
Here are some  issues I have encountered using nistats reports.
* Does not seem to take into account use-provided parameters for thresholding 
* the contrast maps only have the positive part displayed
* multi-dimensional contrasts do not display well.


Discussion:
> the contrast maps only have the positive part displayed

That's due to issue nilearn/nilearn#2397 

> Does not seem to take into account use-provided parameters for thresholding
> multi-dimensional contrasts do not display well.

Examples/code to reproduce?
Some of my comments are wrong. Let me correct them:
- there is a slight inconsistency in the default parameters. default alpha should 0.001 not 0.01, to match the default threshold (3.09)
- nilearn/nilearn#2397 
- the display of multi-dimensional contrasts is not good.

Clearly these 3 things are distinct and thus should be handled in separate PRs. I Can work on it.

I've also encountered an issue with the reports. If the inputs to your second-level analysis are a list of  `FirstLevelModel` instances, there is no way to define first-level contrasts of these models. In this case, defining first-level contrasts are necessary, otherwise `SecondLevelModel.compute_contrasts` will throw an error: 

```
ValueError: If second_level_input was a list of FirstLevelModel, then first_level_contrast is mandatory. It corresponds to the second_level_contrast argument of the compute_contrast method of FirstLevelModel
```

It seems to me that if you pass in a second-level model, it only accepts second-level contrasts. Not sure whether or not to make a separate issue with the impending nilearn merge. 
Thx for reporting.
I think that this is a sepaarte issue, that should be addressed any how.
> Not sure whether or not to make a separate issue with the impending nilearn merge.

I've made a separate issue. It can be worked on in `nilearn.stats` after the merge.
Thanks @kchawla-pi, I'd be happy to work on it once the merge is done!
Other minor issues:

- [x] The top table should not mention fir delay if fir was not used.

![image](https://github.com/nilearn/nilearn/assets/6961185/d6f680e0-c070-4d14-8216-7ce04cea15b2)

- [x] Design matrix title overlaps with picture

![image](https://github.com/nilearn/nilearn/assets/6961185/bb47475c-f70f-4686-8c4a-184a685d3991)

- [ ]  Change the color map used 

unicolor sequential when only positive or negative color (probably bwr_r will be better):

![image](https://github.com/nilearn/nilearn/assets/6961185/4844871c-dd3f-446c-9e7f-721ba5308a1c)

- [ ] bicolor diverging when positive and negative values are present (black_red ?):

![image](https://github.com/nilearn/nilearn/assets/6961185/b9d9fdb7-b849-4f24-b971-0b45cd1e23a2)

- [x] The color map next to the section does not have units. 

- [x] Cluster table alpha does not have an appropriate number of significant figure:

![image](https://github.com/nilearn/nilearn/assets/6961185/d79b4b29-22dc-4e4c-940a-1e9e4ce99867)


All of those are good points, thx.
Sorry for turning this into a "meta-issue / laundry-list". 
Will try to add a checklist in the top post of the issue to make it easier to track.

Resolving patch:
diff --git a/nilearn/glm/first_level/first_level.py b/nilearn/glm/first_level/first_level.py
--- a/nilearn/glm/first_level/first_level.py
+++ b/nilearn/glm/first_level/first_level.py
@@ -73,7 +73,7 @@ def mean_scaling(Y, axis=0):
     mean = Y.mean(axis=axis)
     if (mean == 0).any():
         warn(
-            "Mean values of 0 observed."
+            "Mean values of 0 observed. "
             "The data have probably been centered."
             "Scaling might not work as expected",
             UserWarning,
diff --git a/nilearn/reporting/glm_reporter.py b/nilearn/reporting/glm_reporter.py
--- a/nilearn/reporting/glm_reporter.py
+++ b/nilearn/reporting/glm_reporter.py
@@ -14,6 +14,7 @@
 import warnings
 from collections import OrderedDict
 from collections.abc import Iterable
+from decimal import Decimal
 from html import escape
 
 import numpy as np
@@ -21,6 +22,7 @@
 from matplotlib import pyplot as plt
 
 from nilearn.plotting import plot_glass_brain, plot_roi, plot_stat_map
+from nilearn.plotting.cm import _cmap_d as nilearn_cmaps
 from nilearn.plotting.img_plotting import MNI152TEMPLATE
 from nilearn.plotting.matrix_plotting import (
     plot_contrast_matrix,
@@ -36,6 +38,7 @@
     from nilearn.glm.thresholding import threshold_stats_img
 
 from nilearn._utils import check_niimg
+from nilearn._utils.niimg import safe_get_data
 from nilearn.maskers import NiftiMasker
 from nilearn.reporting.get_clusters_table import get_clusters_table
 from nilearn.reporting.utils import figure_to_svg_quoted
@@ -480,13 +483,15 @@ def _model_attributes_to_dataframe(model):
         "smoothing_fwhm",
         "target_affine",
         "slice_time_ref",
-        "fir_delays",
     ]
     attribute_units = {
         "t_r": "s",
         "high_pass": "Hz",
     }
 
+    if hasattr(model, "hrf_model") and getattr(model, "hrf_model") == "fir":
+        selected_attributes.append("fir_delays")
+
     selected_attributes.sort()
     display_attributes = OrderedDict(
         (attr_name, getattr(model, attr_name))
@@ -573,7 +578,8 @@ def _dmtx_to_svg_url(design_matrices):
         dmtx_text_ = string.Template(dmtx_template_text)
         dmtx_plot = plot_design_matrix(design_matrix)
         dmtx_title = f"Run {dmtx_count}"
-        plt.title(dmtx_title, y=0.987)
+        if len(design_matrices) > 1:
+            plt.title(dmtx_title, y=1.025, x=-0.1)
         dmtx_plot = _resize_plot_inches(dmtx_plot, height_change=0.3)
         url_design_matrix_svg = _plot_to_svg(dmtx_plot)
         # prevents sphinx-gallery & jupyter from scraping & inserting plots
@@ -811,7 +817,7 @@ def _make_stat_maps_contrast_clusters(
         )
         table_details_html = _dataframe_to_html(
             table_details,
-            precision=2,
+            precision=3,
             header=False,
             classes="cluster-details-table",
         )
@@ -877,6 +883,8 @@ def _clustering_params_to_dataframe(
         This is simpler than overloading the class using inheritance,
         especially given limited Python2 use at time of release.
         """
+        if alpha < 0.001:
+            alpha = f"{Decimal(alpha):.2E}"
         if os.sys.version_info.major == 2:
             table_details.update({"alpha": alpha})
         else:
@@ -953,12 +961,28 @@ def _stat_map_to_svg(
         SVG Image Data URL representing a statistical map.
 
     """
+    data = safe_get_data(stat_img, ensure_finite=True)
+    stat_map_min = np.nanmin(data)
+    stat_map_max = np.nanmax(data)
+    symmetric_cbar = True
+    cmap = "bwr"
+    if stat_map_min >= 0.0:
+        symmetric_cbar = False
+        cmap = "red_transparent_full_alpha_range"
+    elif stat_map_max <= 0.0:
+        symmetric_cbar = False
+        cmap = "blue_transparent_full_alpha_range"
+        cmap = nilearn_cmaps[cmap].reversed()
+
     if plot_type == "slice":
         stat_map_plot = plot_stat_map(
             stat_img,
             bg_img=bg_img,
             cut_coords=cut_coords,
             display_mode=display_mode,
+            colorbar=True,
+            cmap=cmap,
+            symmetric_cbar=symmetric_cbar,
             threshold=threshold,
         )
     elif plot_type == "glass":
@@ -967,6 +991,8 @@ def _stat_map_to_svg(
             display_mode=display_mode,
             colorbar=True,
             plot_abs=False,
+            symmetric_cbar=symmetric_cbar,
+            cmap=cmap,
             threshold=threshold,
         )
     else:
@@ -974,6 +1000,21 @@ def _stat_map_to_svg(
             "Invalid plot type provided. "
             "Acceptable options are 'slice' or 'glass'."
         )
+
+    x_label_color = "black"
+    if plot_type == "slice":
+        x_label_color = "white"
+
+    if hasattr(stat_map_plot, "_cbar"):
+        cbar_ax = stat_map_plot._cbar.ax
+        cbar_ax.set_xlabel(
+            "Z score",
+            labelpad=5,
+            fontweight="bold",
+            loc="right",
+            color=x_label_color,
+        )
+
     with pd.option_context("display.precision", 2):
         _add_params_to_plot(table_details, stat_map_plot)
     fig = plt.gcf()


Test patch:
diff --git a/doc/visual_testing/reporter_visual_inspection_suite.py b/doc/visual_testing/reporter_visual_inspection_suite.py
--- a/doc/visual_testing/reporter_visual_inspection_suite.py
+++ b/doc/visual_testing/reporter_visual_inspection_suite.py
@@ -77,6 +77,7 @@ def report_flm_adhd_dmn():
         contrasts=contrasts,
         title="ADHD DMN Report",
         cluster_threshold=15,
+        alpha=0.0009,
         height_control="bonferroni",
         min_distance=8.0,
         plot_type="glass",
@@ -176,6 +177,7 @@ def report_flm_bids_features():
         contrasts="StopSuccess - Go",
         title=title,
         cluster_threshold=3,
+        plot_type="glass",
     )
 
     report.save_as_html(REPORTS_DIR / "flm_bids_features.html")
@@ -252,8 +254,12 @@ def report_slm_oasis():
         oasis_dataset.gray_matter_maps, design_matrix=design_matrix
     )
 
-    # TODO The following fails:
+    # TODO the following crashes
+    # contrast = [np.array([1, 0]), np.array([0, 1])]
     # contrast = [[1, 0, 0], [0, 1, 0]]
+
+    # The following are equivalent
+    # contrast = [np.array([1, 0, 0]), np.array([0, 1, 0])]
     contrast = ["age", "sex"]
 
     report = make_glm_report(
@@ -261,6 +267,7 @@ def report_slm_oasis():
         contrasts=contrast,
         bg_img=datasets.fetch_icbm152_2009()["t1"],
         height_control=None,
+        plot_type="glass",
     )
 
     report.save_as_html(REPORTS_DIR / "slm_oasis.html")