Repository: nilearn/nilearn

Issue:
mosaic
We need a mosaic mode



Discussion:
Cc @chrisfilo

What do you mean by mosaic mode?

Slice view with multiple rows and columns. ATM we are limited to one row
when displaying images.

On Tue, Sep 16, 2014 at 3:08 PM, Alexandre Abraham <notifications@github.com

> wrote:
> 
> What do you mean by mosaic mode?
> 
> —
> Reply to this email directly or view it on GitHub
> https://github.com/nilearn/nilearn/issues/212#issuecomment-55739142.

Is there a critical need for this feature or can we postpone it?

> Is there a critical need for this feature or can we postpone it?

Postpone

OK so I remove it from "first sprint".

I use this kind of mosaic mode often, so I'd like to try and tackle this.  Here's what I'm thinking:
- Create a generic function for mosaic plotting, that takes a bunch of the normal plotting args (title, etc), as well as info about the functions to plot and the shape of the mosaic (aspect_ratio, shape)

``` python
def plot_mosaic(imgs, plot_fn, aspect_ratio=1.3333, shape=None,
                 output_file=None, display_mode='ortho',
                 title=None, threshold='auto',
                 annotate=True,
                 black_bg=False,
                 cmap=None,
                 alpha=0.7):
```

Each argument can be iterable, or a single value.  There will be a loop over each subplot, and the args will be zipped up there and passed to the plot_fn (which can also be a list).  Then I'll do a bit of massaging of the figure to make the subplots look nice.

This function will public for slightly advanced users (hi!).  I also plan to define lightweight wrappers (`plot_epi_mosaic`, etc) for simplified scenarios for less adventurous users  These wrapper functions can do things like set the face color, so that `plot_epi_mosaic` would be all black, for example.

Any feedback on this proposed design is welcome!  I have a function similar to this already as ... I do a lot of mosaic plots.

(edited to remove figure, axes arguments)

I tried using named args above, as I thought it would be requested, but I think using `**kwargs` is better--different functions may require different parameters.  The wrapper functions (e.g. `plot_epi_mosaic`) can have a more well-defined parameter list.

Should it use subplots or rather just rearrange slices into one big image, similarly to [here](https://github.com/gallantlab/pycortex/blob/649979c11436a9e95f3fcf34acd67ceb7b2b5e17/cortex/volume.py#L85)?

Mosaic does sound cool.

Just some anecdotal evidence: plot_stat_map+subplot was comfortable plotting 40 times x/y/z slices into a single image...

@eickenberg the pycortex code seems to be dumping slices directly to an image, so more simple than what is intended here.   Are you suggesting that I render each subplot as an image, build a single grid from that, and show that?

I think supporting both is useful; those who want to tweak the results afterwards will want to use subplot; those who want to plot a lot of images and python starts choking on the size will want to render to image.

I suggest doing as subplot only as a first pass.  Subplotting will allow wrapper functions to tweak the plot (if desired) before rendering to the user.  Rendering to an image is definitely useful but doesn't seem immediately necessary to me, and could be a second PR.

How does that sound?

The one big image thing is only for quickness, at least in my experience it is a lot faster to make a big panel of images and show it than 96 subplots. I put the reference to the pycortex mosaic just to inform of its existsnce. Experienced users can just take the data and magic into a panel with the right calls to  `reshape`, `transpose` and `reshape`.

For rendering nice plots, it the mosaic should build on existing plots that can only extend horizontally and add the second axis.

> For rendering nice plots, it the mosaic should build on existing plots that can only extend horizontally and add the second axis.

@eickenberg I'm newer to this code; can you write a bit more about this?  Are there plots that only extend horizontally, or you want me to prevent that?  And what axis is the second axis you're thinking of?

Any feedback / suggestions welcome.  See https://github.com/nilearn/nilearn/issues/461#issuecomment-75882292 for a current image of the direction I'm heading in.

One more enhancement I am thinking of: a "grid", that would allow one to span over multiple subplots very simply.  It's a very small extension from what I have so far.

Most of the issues in nilearn are not solved not because of the code involved (we all have code to do mosaic plotting) but because we don't know what is the best way to expose that to the user. So I think that we should settle down before taking any decision.

First, the usage. I guess that it more likely to be used to display beta maps, a probabilistic atlas (typically ICA components), or several hard atlases for comparison (ward, kmeans...). Note that those could be the examples.

So what do we want? Well, my first feature request for that would be having a common colorbar / legend. And this the non trivial part of my own mosaic code. Because maybe we just want an informative colorbar (no values, no problems, we just have to normalize the images) or maybe we want to have the same valued colorbar for all which means that we should automatically set vmax to the max across all maps.

Now for the implementation. We have to choose whether we several small functions or a "god" one. I prefer small functions but a common colorbar is easier to implement in a general function. Also, a concern is that matplotlib functions can already take fixed values or iterable depending on the expected behavior. For example, `plot_connectome` can take one color if you want your nodes to be the same color, or a list of colors if you want to give a specific color to each node. So, if the number of nodes is equal to the number of plots in your grid, you won't be able to know if there is one option per plot or the same list of option for all the plots.

Finally, like @banilo, I don't think that performances are a problem. One easy thing is to use `rasterized=True` if there is a lot of plots (we should test on a regular desktop what id the limit of reasonably plottable images). If it is still too long, we should advise people to switch to qtagg (or gtkagg) backend because it is much faster.

Also, note that the original issue was simply to span slices in 'z' mode (for example) on several rows, not to have several plots in one figure.

@AlexandreAbraham thanks for the comments and guidance!  I figured y'all had your own things and glad to understand better what could be valuable for the group.

I agree on the colorbar issue--almost added a comment about it yesterday, after posting the result.  It should be easy to implement either within this "god" function or as a utility function that others call into.  

Also, the "grid" feature I mentioned yesterday should take care of the "span slices in 'z' mode" case.

So, I'm hopeful that all this can get taken care of in a function that's powerful, but not overly complex.  I'll try the above out today, and would look forward to any further comments.


Resolving patch:
diff --git a/examples/01_plotting/plot_demo_more_plotting.py b/examples/01_plotting/plot_demo_more_plotting.py
--- a/examples/01_plotting/plot_demo_more_plotting.py
+++ b/examples/01_plotting/plot_demo_more_plotting.py
@@ -134,6 +134,34 @@
                        cut_coords=[36, -27, 60],
                        title="display_mode='tiled'")
 
+########################################
+# Visualizing three views along multiple rows and columns
+# -------------------------------------------------------------------------
+# display_mode='mosaic' for sagittal, coronal and axial view with
+# default option i.e. cut_coords=None
+
+plotting.plot_stat_map(stat_img, display_mode='mosaic',
+                       title="display_mode='mosaic' default cut_coords")
+
+########################################
+# Now, changing the number of slices along columns
+# -------------------------------------------------------------------------
+# display_mode='mosaic' for sagittal, coronal and axial view with
+# number of slices specified as integer i.e. cut_coords=3
+
+plotting.plot_stat_map(stat_img, display_mode='mosaic',
+                       cut_coords=3,
+                       title="display_mode='mosaic' with cut_coords=3")
+
+########################################
+# Now, another way of limiting the number of slices along rows and columns
+# -------------------------------------------------------------------------
+# display_mode='mosaic' for sagittal, coronal and axial view with
+# number of slices specified as tuple of length 3
+
+plotting.plot_stat_map(stat_img, display_mode='mosaic',
+                       cut_coords=(5, 4, 10),
+                       title="display_mode='mosaic' with cut_coords as tuple")
 ###############################################################################
 # Demonstrating various display features
 # ---------------------------------------
diff --git a/nilearn/plotting/displays.py b/nilearn/plotting/displays.py
--- a/nilearn/plotting/displays.py
+++ b/nilearn/plotting/displays.py
@@ -692,7 +692,12 @@ def title(self, text, x=0.01, y=0.99, size=15, color=None, bgcolor=None,
         if bgcolor is None:
             bgcolor = 'w' if self._black_bg else 'k'
         if hasattr(self, '_cut_displayed'):
-            first_axe = self._cut_displayed[0]
+            # Adapt to the case of mosaic plotting
+            if isinstance(self.cut_coords, dict):
+                first_axe = self._cut_displayed[-1]
+                first_axe = (first_axe, self.cut_coords[first_axe][0])
+            else:
+                first_axe = self._cut_displayed[0]
         else:
             first_axe = self.cut_coords[0]
         ax = self.axes[first_axe].ax
@@ -1807,8 +1812,221 @@ class YZSlicer(OrthoSlicer):
     _cut_displayed = 'yz'
 
 
+class MosaicSlicer(BaseSlicer):
+    """ A class to create 3 axes for plotting cuts of 3D maps,
+    in multiple rows and columns.
+
+    Attributes
+    ----------
+    axes : dictionary of axes
+        The 3 axes used to plot multiple views.
+
+    frame_axes : axes
+        The axes framing the whole set of views.
+
+    """
+    _cut_displayed = 'yxz'
+    _axes_class = CutAxes
+    _default_figsize = [11.1, 7.2]
+
+    @classmethod
+    def find_cut_coords(self, img=None, threshold=None, cut_coords=None):
+        """Instantiate the slicer and find cut coordinates for mosaic plotting.
+
+        Parameters
+        ----------
+        img : 3D Nifti1Image, optional
+            The brain image.
+
+        threshold : float, optional
+            The lower threshold to the positive activation. If None, the
+            activation threshold is computed using the 80% percentile of
+            the absolute value of the map.
+
+        cut_coords : list/tuple of 3 floats, integer, optional
+            xyz world coordinates of cuts. If cut_coords
+            are not provided, 7 coordinates of cuts are automatically
+            calculated.
+
+        Returns
+        -------
+        cut_coords : dict
+            xyz world coordinates of cuts in a direction. Each key
+            denotes the direction.
+        """
+        if cut_coords is None:
+            cut_coords = 7
+
+        if (not isinstance(cut_coords, collections.abc.Sequence) and
+                isinstance(cut_coords, numbers.Number)):
+            cut_coords = [cut_coords] * 3
+            cut_coords = self._find_cut_coords(img, cut_coords,
+                                               self._cut_displayed)
+        else:
+            if len(cut_coords) != len(self._cut_displayed):
+                raise ValueError('The number cut_coords passed does not'
+                                 ' match the display_mode. Mosaic plotting '
+                                 'expects tuple of length 3.' )
+            cut_coords = [cut_coords['xyz'.find(c)]
+                for c in sorted(self._cut_displayed)]
+            cut_coords = self._find_cut_coords(img, cut_coords,
+                                               self._cut_displayed)
+        return cut_coords
+
+    def _find_cut_coords(img, cut_coords, cut_displayed):
+        """ Find slicing positions along a given axis.
+
+            Helper function to find_cut_coords.
+
+        Parameters
+        ----------
+        img : 3D Nifti1Image
+            The brain image.
+
+        cut_coords : list/tuple of 3 floats, integer, optional
+            xyz world coordinates of cuts.
+
+        cut_displayed : str
+            Sectional directions 'yxz'
+
+        Returns
+        -------
+        cut_coords : 1D array of length specified in n_cuts
+            The computed cut_coords.
+        """
+        coords = dict()
+        if img is None or img is False:
+            bounds = ((-40, 40), (-30, 30), (-30, 75))
+            for direction, n_cuts in zip(sorted(cut_displayed),
+                                         cut_coords):
+                lower, upper = bounds['xyz'.index(direction)]
+                coords[direction] = np.linspace(lower, upper,
+                                                n_cuts).tolist()
+        else:
+            for direction, n_cuts in zip(sorted(cut_displayed),
+                                         cut_coords):
+                coords[direction] = find_cut_slices(img, direction=direction,
+                                                    n_cuts=n_cuts)
+        return coords
+
+    def _init_axes(self, **kwargs):
+        """Initializes and places axes for display of 'xyz' multiple cuts.
+
+        Parameters
+        ----------
+        kwargs:
+            additional arguments to pass to self._axes_class
+
+        """
+        if not isinstance(self.cut_coords, dict):
+            self.cut_coords = self.find_cut_coords(cut_coords=self.cut_coords)
+
+        if len(self.cut_coords) != len(self._cut_displayed):
+            raise ValueError('The number cut_coords passed does not'
+                             ' match the mosaic mode')
+        x0, y0, x1, y1 = self.rect
+
+        # Create our axes:
+        self.axes = dict()
+        # portions for main axes
+        fraction = y1 / len(self.cut_coords)
+        height = fraction
+        for index, direction in enumerate(self._cut_displayed):
+            coords = self.cut_coords[direction]
+            # portions allotment for each of 'x', 'y', 'z' coordinate
+            fraction_c = 1. / len(coords)
+            fh = self.frame_axes.get_figure()
+            indices = [x0, fraction * index * (y1 - y0) + y0,
+                       x1, fraction * (y1 - y0)]
+            ax = fh.add_axes(indices)
+            ax.axis('off')
+            this_x0, this_y0, this_x1, this_y1 = indices
+            for index_c, coord in enumerate(coords):
+                coord = float(coord)
+                fh_c = ax.get_figure()
+                # indices for each sub axes within main axes
+                indices = [fraction_c * index_c * (this_x1 - this_x0) + this_x0,
+                           this_y0,
+                           fraction_c * (this_x1 - this_x0),
+                           height]
+                ax = fh_c.add_axes(indices)
+                ax.axis('off')
+                display_ax = self._axes_class(ax, direction,
+                                              coord, **kwargs)
+                self.axes[(direction, coord)] = display_ax
+                ax.set_axes_locator(self._locator)
+
+    def _locator(self, axes, renderer):
+        """ The locator function used by matplotlib to position axes.
+            Here we put the logic used to adjust the size of the axes.
+        """
+        x0, y0, x1, y1 = self.rect
+        display_ax_dict = self.axes
+
+        if self._colorbar:
+            adjusted_width = self._colorbar_width / len(self.axes)
+            right_margin = self._colorbar_margin['right'] / len(self.axes)
+            ticks_margin = self._colorbar_margin['left'] / len(self.axes)
+            x1 = x1 - (adjusted_width + right_margin + ticks_margin)
+
+        # capture widths for each axes for anchoring Bbox
+        width_dict = dict()
+        for direction in self._cut_displayed:
+            this_width = dict()
+            for display_ax in display_ax_dict.values():
+                if direction == display_ax.direction:
+                    bounds = display_ax.get_object_bounds()
+                    if not bounds:
+                        # This happens if the call to _map_show was not
+                        # successful. As it happens asynchronously (during a
+                        # refresh of the figure) we capture the problem and
+                        # ignore it: it only adds a non informative traceback
+                        bounds = [0, 1, 0, 1]
+                    xmin, xmax, ymin, ymax = bounds
+                    this_width[display_ax.ax] = (xmax - xmin)
+            total_width = float(sum(this_width.values()))
+            for ax, w in this_width.items():
+                width_dict[ax] = w / total_width * (x1 - x0)
+
+        left_dict = dict()
+        # bottom positions in Bbox according to cuts
+        bottom_dict = dict()
+        # fraction is divided by the cut directions 'y', 'x', 'z'
+        fraction = y1 / len(self._cut_displayed)
+        height_dict = dict()
+        for index, direction in enumerate(self._cut_displayed):
+            left = float(x0)
+            this_height = fraction + fraction * index
+            for coord, display_ax in display_ax_dict.items():
+                if direction == display_ax.direction:
+                    left_dict[display_ax.ax] = left
+                    this_width = width_dict[display_ax.ax]
+                    left += this_width
+                    bottom_dict[display_ax.ax] = fraction * index * (y1 - y0)
+                    height_dict[display_ax.ax] = this_height
+        return transforms.Bbox([[left_dict[axes], bottom_dict[axes]],
+                                [left_dict[axes] + width_dict[axes],
+                                 height_dict[axes]]])
+
+
+    def draw_cross(self, cut_coords=None, **kwargs):
+        """ Draw a crossbar on the plot to show where the cut is
+        performed.
+
+        Parameters
+        ----------
+        cut_coords: 3-tuple of floats, optional
+            The position of the cross to draw. If none is passed, the
+            ortho_slicer's cut coordinates are used.
+        kwargs:
+            Extra keyword arguments are passed to axhline
+        """
+        return
+
+
 SLICERS = dict(ortho=OrthoSlicer,
                tiled=TiledSlicer,
+               mosaic=MosaicSlicer,
                xz=XZSlicer,
                yz=YZSlicer,
                yx=YXSlicer,
diff --git a/nilearn/plotting/img_plotting.py b/nilearn/plotting/img_plotting.py
--- a/nilearn/plotting/img_plotting.py
+++ b/nilearn/plotting/img_plotting.py
@@ -532,18 +532,23 @@ def plot_anat(anat_img=MNI152TEMPLATE, cut_coords=None,
         coordinates of each cut in the corresponding direction.
         If None is given, the cuts is calculated automaticaly.
         If display_mode is 'x', 'y' or 'z', cut_coords can be an integer,
-        in which case it specifies the number of cuts to perform
+        in which case it specifies the number of cuts to perform.
+        If display_mode is 'mosaic', and the number of cuts is the same
+        for all directions, cut_coords can be specified as an integer.
+        It can also be a length 3 tuple specifying the number of cuts for
+        every direction if these are different.
 
     output_file : string, or None, optional
         The name of an image file to export the plot to. Valid extensions
         are .png, .pdf, .svg. If output_file is not None, the plot
         is saved to a file, and the display is closed.
 
-    display_mode : {'ortho', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
+    display_mode : {'ortho', 'tiled', 'mosaic', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
         Choose the direction of the cuts: 'x' - sagittal, 'y' - coronal,
         'z' - axial, 'ortho' - three cuts are performed in orthogonal
         directions, 'tiled' - three cuts are performed
-        and arranged in a 2x2 grid. Default='ortho'.
+        and arranged in a 2x2 grid, 'mosaic' - three cuts
+        are performed along multiple rows and columns, Default='ortho'.
 
     figure : integer or matplotlib figure, optional
         Matplotlib figure used or its number. If None is given, a
@@ -640,18 +645,23 @@ def plot_epi(epi_img=None, cut_coords=None, output_file=None,
         coordinates of each cut in the corresponding direction.
         If None is given, the cuts is calculated automaticaly.
         If display_mode is 'x', 'y' or 'z', cut_coords can be an integer,
-        in which case it specifies the number of cuts to perform
+        in which case it specifies the number of cuts to perform.
+        If display_mode is 'mosaic', and the number of cuts is the same
+        for all directions, cut_coords can be specified as an integer.
+        It can also be a length 3 tuple specifying the number of cuts for
+        every direction if these are different.
 
     output_file : string, or None, optional
         The name of an image file to export the plot to. Valid extensions
         are .png, .pdf, .svg. If output_file is not None, the plot
         is saved to a file, and the display is closed.
 
-    display_mode : {'ortho', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
+    display_mode : {'ortho', 'tiled', 'mosaic', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
         Choose the direction of the cuts: 'x' - sagittal, 'y' - coronal,
         'z' - axial, 'ortho' - three cuts are performed in orthogonal
         directions, 'tiled' - three cuts are performed
-        and arranged in a 2x2 grid. Default='ortho'.
+        and arranged in a 2x2 grid. 'mosaic' - three cuts are
+        performed along multiple rows and columns. Default='ortho'.
 
     figure : integer or matplotlib figure, optional
         Matplotlib figure used or its number. If None is given, a
@@ -787,17 +797,22 @@ def plot_roi(roi_img, bg_img=MNI152TEMPLATE, cut_coords=None,
         For display_mode == 'x', 'y', or 'z', then these are the
         coordinates of each cut in the corresponding direction.
         If None is given, the cuts is calculated automaticaly.
+        If display_mode is 'mosaic', and the number of cuts is the same
+        for all directions, cut_coords can be specified as an integer.
+        It can also be a length 3 tuple specifying the number of cuts for
+        every direction if these are different.
 
     output_file : string, or None, optional
         The name of an image file to export the plot to. Valid extensions
         are .png, .pdf, .svg. If output_file is not None, the plot
         is saved to a file, and the display is closed.
 
-    display_mode : {'ortho', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
+    display_mode : {'ortho', 'mosaic', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
         Choose the direction of the cuts: 'x' - sagittal, 'y' - coronal,
         'z' - axial, 'ortho' - three cuts are performed in orthogonal
         directions, 'tiled' - three cuts are performed
-        and arranged in a 2x2 grid. Default='ortho'.
+        and arranged in a 2x2 grid, 'mosaic' - three cuts
+        are performed along multiple rows and columns, Default='ortho'.
 
     figure : integer or matplotlib figure, optional
         Matplotlib figure used or its number. If None is given, a
@@ -983,19 +998,23 @@ def plot_prob_atlas(maps_img, bg_img=MNI152TEMPLATE, view_type='auto',
         coordinates of each cut in the corresponding direction.
         If None is given, the cuts is calculated automaticaly.
         If display_mode is 'x', 'y' or 'z', cut_coords can be an integer,
-        in which case it specifies the number of cuts to perform
+        in which case it specifies the number of cuts to perform.
+        If display_mode is 'mosaic', and the number of cuts is the same
+        for all directions, cut_coords can be specified as an integer.
+        It can also be a length 3 tuple specifying the number of cuts for
+        every direction if these are different.
 
     output_file : string, or None, optional
         The name of an image file to export the plot to. Valid extensions
         are .png, .pdf, .svg. If output_file is not None, the plot
         is saved to a file, and the display is closed.
 
-    display_mode : {'ortho', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
+    display_mode : {'ortho', 'tiled', 'mosaic', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
         Choose the direction of the cuts: 'x' - sagittal, 'y' - coronal,
         'z' - axial, 'ortho' - three cuts are performed in orthogonal
         directions, 'tiled' - three cuts are performed
-        and arranged in a 2x2 grid.
-        Default='ortho'.
+        and arranged in a 2x2 grid, 'mosaic' - three cuts are performed along
+        multiple rows and columns, Default='ortho'.
 
     figure : integer or matplotlib figure, optional
         Matplotlib figure used or its number. If None is given, a
@@ -1179,19 +1198,23 @@ def plot_stat_map(stat_map_img, bg_img=MNI152TEMPLATE, cut_coords=None,
         coordinates of each cut in the corresponding direction.
         If None is given, the cuts is calculated automaticaly.
         If display_mode is 'x', 'y' or 'z', cut_coords can be an integer,
-        in which case it specifies the number of cuts to perform
+        in which case it specifies the number of cuts to perform.
+        If display_mode is 'mosaic', and the number of cuts is the same
+        for all directions, cut_coords can be specified as an integer.
+        It can also be a length 3 tuple specifying the number of cuts for
+        every direction if these are different.
 
     output_file : string, or None, optional
         The name of an image file to export the plot to. Valid extensions
         are .png, .pdf, .svg. If output_file is not None, the plot
         is saved to a file, and the display is closed.
 
-    display_mode : {'ortho', 'tiled', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
+    display_mode : {'ortho', 'tiled', 'mosaic', 'x', 'y', 'z', 'yx', 'xz', 'yz'}, optional
         Choose the direction of the cuts: 'x' - sagittal, 'y' - coronal,
         'z' - axial, 'ortho' - three cuts are performed in orthogonal
         directions, 'tiled' - three cuts are performed
-        and arranged in a 2x2 grid.
-        Default='ortho'.
+        and arranged in a 2x2 grid, 'mosaic' - three cuts
+        are performed along multiple rows and columns, Default='ortho'.
 
     colorbar : boolean, optional
         If True, display a colorbar on the right of the plots. Default=True.


Test patch:
diff --git a/nilearn/plotting/tests/test_displays.py b/nilearn/plotting/tests/test_displays.py
--- a/nilearn/plotting/tests/test_displays.py
+++ b/nilearn/plotting/tests/test_displays.py
@@ -4,10 +4,12 @@
 
 import matplotlib.pyplot as plt
 import nibabel
+import pytest
 import numpy as np
 
 from nilearn.plotting.displays import OrthoSlicer, XSlicer, OrthoProjector
 from nilearn.plotting.displays import TiledSlicer
+from nilearn.plotting.displays import MosaicSlicer
 from nilearn.plotting.displays import LZRYProjector
 from nilearn.plotting.displays import LYRZProjector
 from nilearn.datasets import load_mni152_template
@@ -21,6 +23,7 @@ def test_demo_ortho_slicer():
     oslicer = OrthoSlicer(cut_coords=(0, 0, 0))
     img = load_mni152_template()
     oslicer.add_overlay(img, cmap=plt.cm.gray)
+    oslicer.title("display mode is ortho")
     oslicer.close()
 
 
@@ -53,6 +56,65 @@ def test_tiled_slicer():
     slicer.close()
 
 
+def test_mosaic_slicer():
+    img = load_mni152_template()
+    # default cut_coords=None
+    slicer = MosaicSlicer.init_with_figure(img=img)
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+    # Forcing a layout here, to test the locator code
+    with tempfile.TemporaryFile() as fp:
+        slicer.savefig(fp)
+
+    # cut_coords as an integer
+    slicer = MosaicSlicer.init_with_figure(img=img, cut_coords=4)
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+    for d in ['x', 'y', 'z']:
+        assert d in slicer.cut_coords
+        assert len(slicer.cut_coords[d]) == 4
+    # cut_coords as a tuple
+    slicer = MosaicSlicer.init_with_figure(img=img, cut_coords=(4, 5, 2))
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+    assert len(slicer.cut_coords['x']) == 4
+    assert len(slicer.cut_coords['y']) == 5
+    assert len(slicer.cut_coords['z']) == 2
+    # test title
+    slicer.title('Showing mosaic mode')
+
+    # test when img is None or False while initializing figure
+    slicer = MosaicSlicer.init_with_figure(img=None, cut_coords=None)
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+    # same test but cut_coords as integer and tuple
+    slicer = MosaicSlicer.init_with_figure(img=None, cut_coords=5)
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+    slicer = MosaicSlicer.init_with_figure(img=None, cut_coords=(1, 1, 1))
+    slicer.add_overlay(img, cmap=plt.cm.gray, colorbar=True)
+
+    # assert raises ValueError
+    pytest.raises(ValueError, MosaicSlicer.init_with_figure,
+                  img=None, cut_coords=(5, 4))
+
+    slicer.close()
+
+
+def test_demo_mosaic_slicer():
+    # cut_coords as tuple of length 3
+    mslicer = MosaicSlicer(cut_coords=(1, 1, 1))
+    img = load_mni152_template()
+    mslicer.add_overlay(img, cmap=plt.cm.gray)
+    # cut_coords as integer
+    mslicer = MosaicSlicer(cut_coords=5)
+    mslicer.add_overlay(img, cmap=plt.cm.gray)
+    # cut_coords as dictionary
+    mslicer = MosaicSlicer(cut_coords={'x': [10, 20],
+                                       'y': [30, 40],
+                                       'z': [15, 16]})
+    mslicer.add_overlay(img, cmap=plt.cm.gray)
+    assert mslicer.cut_coords == {'x': [10, 20], 'y': [30, 40], 'z': [15, 16]}
+    # assert raises a ValueError
+    pytest.raises(ValueError, MosaicSlicer, cut_coords=(2, 3))
+    mslicer.close()
+
+
 def test_demo_ortho_projector():
     # This is only a smoke test
     img = load_mni152_template()
diff --git a/nilearn/plotting/tests/test_img_plotting.py b/nilearn/plotting/tests/test_img_plotting.py
--- a/nilearn/plotting/tests/test_img_plotting.py
+++ b/nilearn/plotting/tests/test_img_plotting.py
@@ -176,6 +176,19 @@ def test_plot_functions(testdata_3d, testdata_4d, tmpdir):
     plt.close()
 
 
+def test_plot_functions_mosaic_mode(testdata_3d):
+    img_3d = testdata_3d['img']
+
+    # smoke-test for 3D plotting functions with display_mode='mosaic'
+    for cut_coords in [None, 5, (5, 4, 3)]:
+        for plot_func in [plot_anat, plot_img, plot_stat_map, plot_epi,
+                          plot_roi]:
+            plot_func(img_3d, display_mode='mosaic',
+                      title='mosaic mode', cut_coords=cut_coords)
+
+    plt.close()
+
+
 def test_plot_glass_brain(testdata_3d, tmpdir):
     img = testdata_3d['img']
 
@@ -1003,6 +1016,18 @@ def test_invalid_in_display_mode_tiled_cut_coords_all_plots(testdata_3d):
             plot_func(img, display_mode='tiled', cut_coords=(2, 2))
 
 
+def test_invalid_in_display_mode_mosaic_cut_coords_all_plots(testdata_3d):
+    img = testdata_3d['img']
+
+    for plot_func in [plot_img, plot_anat, plot_roi, plot_epi,
+                      plot_stat_map, plot_prob_atlas]:
+        with pytest.raises(ValueError,
+                           match="The number cut_coords passed does not "
+                                 "match the display_mode"
+                           ):
+            plot_func(img, display_mode='mosaic', cut_coords=(2, 2))
+
+
 def test_outlier_cut_coords():
     """ Test to plot a subset of a large set of cuts found for a small area."""
     bg_img = load_mni152_template()