Repository: sphinx-gallery/sphinx-gallery

Issue:
More informative "alt" attribute for image tags
The alternative text for generated HTML `img`  tags could be more informative. Currently it's the filename of the png file.

Instead one could use the title of the example or even better, the "title" attribute of the underlying matplotlib figure when available (and not empty).

Discussion:
lucyleeow: Note matplotlib's `plt.title` can be in different locs (and does not overwrite if different locs are used: https://matplotlib.org/3.1.1/gallery/text_labels_and_annotations/titles_demo.html#sphx-glr-gallery-text-labels-and-annotations-titles-demo-py). Thus we cycle through different locs.

I realised that we must keep `figure_rst` backwards compatible for user generated scrapers, thus the complicated workaround for multi-figures in `matplotlib_scraper`

Read for review @larsoner 
lucyleeow: @ogrisel added titles here: https://1453-25860190-gh.circle-artifacts.com/0/rtd_html/auto_examples/plot_1_exp.html#sphx-glr-auto-examples-plot-1-exp-py
choldgraf: got a travis failure here: https://travis-ci.org/github/sphinx-gallery/sphinx-gallery/jobs/680921561#L1596

but other than that, this is a good idea!
ogrisel: > @ogrisel added titles here: https://1453-25860190-gh.circle-artifacts.com/0/rtd_html/auto_examples/plot_1_exp.html#sphx-glr-auto-examples-plot-1-exp-py

This is a great improvement @lucyleeow ! It would be even greater to similarly change the alt text of the thumbnails of the index page of the gallery:

https://1453-25860190-gh.circle-artifacts.com/0/rtd_html/auto_examples/index.html
lucyleeow: @choldgraf unfortunately that is due to https://github.com/sphinx-doc/sphinx/issues/7570

@ogrisel sure. I think in a different PR. Does the title of the example sound reasonable?
ogrisel: > @ogrisel sure. I think in a different PR. Does the title of the example sound reasonable?

Yes on both points.

Patch:
diff --git a/doc/configuration.rst b/doc/configuration.rst
index a0f02b5..023f287 100644
--- a/doc/configuration.rst
+++ b/doc/configuration.rst
@@ -546,6 +546,10 @@ To remove the comment from the rendered example set the option::
         'remove_config_comments': True,
     }
 
+This only removes configuration comments from code blocks, not from text
+blocks. However, note that technically, configuration comments will work when
+put in either code blocks or text blocks.
+
 .. _own_notebook_cell:
 
 Add your own first and last notebook cell
diff --git a/doc/syntax.rst b/doc/syntax.rst
index 7a1fdfd..4111d32 100644
--- a/doc/syntax.rst
+++ b/doc/syntax.rst
@@ -69,18 +69,26 @@ rST in the built gallery examples. For example::
   print('my variable plus 2 is {}'.format(myvariable + 2))
 
 The ``#%%`` and ``# %%`` syntax is consistent with the 'code block' (or
-'code cell') separator syntax in `Jupyter VSCode plugin
-<https://code.visualstudio.com/docs/python/jupyter-support-py>`_, `Jupytext
-<https://jupytext.readthedocs.io/en/latest/introduction.html>`_, `Pycharm
+'code cell') separator syntax in `Visual Studio Code Python extension
+<https://code.visualstudio.com/docs/python/jupyter-support-py#_jupyter-code-cells>`_,
+`Visual Studio Python Tools
+<https://docs.microsoft.com/en-us/visualstudio/python/python-interactive-repl-in-visual-studio?view=vs-2019#work-with-code-cells>`_,
+`Jupytext
+<https://jupytext.readthedocs.io/en/latest/formats.html#the-percent-format>`_,
+`Pycharm Professional
 <https://www.jetbrains.com/help/pycharm/running-jupyter-notebook-cells.html>`_,
 `Hydrogen plugin (for Atom)
-<https://nteract.gitbooks.io/hydrogen/>`_ and `Spyder
-<https://docs.spyder-ide.org/editor.html>`_. Note that although the
+<https://nteract.gitbooks.io/hydrogen/docs/Usage/Cells.html#example-definitions>`_
+and `Spyder
+<https://docs.spyder-ide.org/editor.html#defining-code-cells>`_.
+Note that although the
 documentation may only mention one of ``#%%`` or ``# %%``, in practice both
-work in these editors. In these IDEs (or with these IDE plugins), ``#%%`` or
+work. With these editors/IDEs, ``#%%`` or
 ``# %%`` at the start of a line signifies the start of a new code block.
-Code within a code block can be easily executed together, at the same time. This
-functionality can be helpful when writing a Sphinx-Gallery ``.py`` example as
+Code blocks allow you to separate your code into chunks, like in Jupyter
+Notebooks. All the code within a code block can be easily executed together.
+This functionality can be helpful when writing a Sphinx-Gallery ``.py``
+example as
 the blocks allow you to easily create pairs of subsequent Sphinx-Gallery text
 and code blocks.
 
diff --git a/examples/plot_1_exp.py b/examples/plot_1_exp.py
index 3872067..b0f1c29 100644
--- a/examples/plot_1_exp.py
+++ b/examples/plot_1_exp.py
@@ -29,11 +29,13 @@ def main():
     plt.plot(x, y)
     plt.xlabel('$x$')
     plt.ylabel('$\exp(x)$')
+    plt.title('Exponential function')
 
     plt.figure()
     plt.plot(x, -np.exp(-x))
     plt.xlabel('$x$')
     plt.ylabel('$-\exp(-x)$')
+    plt.title('Negative exponential function')
     # To avoid matplotlib text output
     plt.show()
 
diff --git a/sphinx_gallery/scrapers.py b/sphinx_gallery/scrapers.py
index b43d122..a56b141 100644
--- a/sphinx_gallery/scrapers.py
+++ b/sphinx_gallery/scrapers.py
@@ -13,6 +13,8 @@ live in modules that will support them (e.g., PyVista, Plotly).
 
 import os
 import sys
+import re
+from textwrap import indent
 
 from .utils import scale_image
 
@@ -47,6 +49,23 @@ def _import_matplotlib():
     return matplotlib, plt
 
 
+def _matplotlib_fig_titles(fig):
+    titles = []
+    # get supertitle if exists
+    suptitle = getattr(fig, "_suptitle", None)
+    if suptitle is not None:
+        titles.append(suptitle.get_text())
+    # get titles from all axes, for all locs
+    title_locs = ['left', 'center', 'right']
+    for ax in fig.axes:
+        for loc in title_locs:
+            text = ax.get_title(loc=loc)
+            if text:
+                titles.append(text)
+    fig_titles = ', '.join(titles)
+    return fig_titles
+
+
 def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
     """Scrape Matplotlib images.
 
@@ -73,7 +92,7 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
     """
     matplotlib, plt = _import_matplotlib()
     image_path_iterator = block_vars['image_path_iterator']
-    image_paths = list()
+    image_rsts = []
     for fig_num, image_path in zip(plt.get_fignums(), image_path_iterator):
         if 'format' in kwargs:
             image_path = '%s.%s' % (os.path.splitext(image_path)[0],
@@ -81,6 +100,8 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
         # Set the fig_num figure as the current figure as we can't
         # save a figure that's not the current figure.
         fig = plt.figure(fig_num)
+        # get fig titles
+        fig_titles = _matplotlib_fig_titles(fig)
         to_rgba = matplotlib.colors.colorConverter.to_rgba
         # shallow copy should be fine here, just want to avoid changing
         # "kwargs" for subsequent figures processed by the loop
@@ -92,9 +113,20 @@ def matplotlib_scraper(block, block_vars, gallery_conf, **kwargs):
                     attr not in kwargs:
                 these_kwargs[attr] = fig_attr
         fig.savefig(image_path, **these_kwargs)
-        image_paths.append(image_path)
+        image_rsts.append(
+            figure_rst([image_path], gallery_conf['src_dir'], fig_titles))
     plt.close('all')
-    return figure_rst(image_paths, gallery_conf['src_dir'])
+    rst = ''
+    if len(image_rsts) == 1:
+        rst = image_rsts[0]
+    elif len(image_rsts) > 1:
+        image_rsts = [re.sub(r':class: sphx-glr-single-img',
+                             ':class: sphx-glr-multi-img',
+                             image) for image in image_rsts]
+        image_rsts = [HLIST_IMAGE_MATPLOTLIB + indent(image, u' ' * 6)
+                      for image in image_rsts]
+        rst = HLIST_HEADER + ''.join(image_rsts)
+    return rst
 
 
 def mayavi_scraper(block, block_vars, gallery_conf):
@@ -242,7 +274,7 @@ def save_figures(block, block_vars, gallery_conf):
     return all_rst
 
 
-def figure_rst(figure_list, sources_dir):
+def figure_rst(figure_list, sources_dir, fig_titles=''):
     """Generate RST for a list of image filenames.
 
     Depending on whether we have one or more figures, we use a
@@ -254,6 +286,9 @@ def figure_rst(figure_list, sources_dir):
         List of strings of the figures' absolute paths.
     sources_dir : str
         absolute path of Sphinx documentation sources
+    fig_titles : str
+        Titles of figures, empty string if no titles found. Currently
+        only supported for matplotlib figures, default = ''.
 
     Returns
     -------
@@ -264,15 +299,26 @@ def figure_rst(figure_list, sources_dir):
     figure_paths = [os.path.relpath(figure_path, sources_dir)
                     .replace(os.sep, '/').lstrip('/')
                     for figure_path in figure_list]
+    # Get alt text
+    alt = ''
+    if fig_titles:
+        alt = fig_titles
+    elif figure_list:
+        file_name = os.path.split(figure_list[0])[1]
+        # remove ext & 'sphx_glr_' from start & n#'s from end
+        file_name_noext = os.path.splitext(file_name)[0][9:-4]
+        # replace - & _ with \s
+        file_name_final = re.sub(r'[-,_]', ' ', file_name_noext)
+        alt = file_name_final
+
     images_rst = ""
     if len(figure_paths) == 1:
         figure_name = figure_paths[0]
-        images_rst = SINGLE_IMAGE % figure_name
+        images_rst = SINGLE_IMAGE % (figure_name, alt)
     elif len(figure_paths) > 1:
         images_rst = HLIST_HEADER
         for figure_name in figure_paths:
-            images_rst += HLIST_IMAGE_TEMPLATE % figure_name
-
+            images_rst += HLIST_IMAGE_TEMPLATE % (figure_name, alt)
     return images_rst
 
 
@@ -284,15 +330,21 @@ HLIST_HEADER = """
 
 """
 
+HLIST_IMAGE_MATPLOTLIB = """
+    *
+"""
+
 HLIST_IMAGE_TEMPLATE = """
     *
 
       .. image:: /%s
-            :class: sphx-glr-multi-img
+          :alt: %s
+          :class: sphx-glr-multi-img
 """
 
 SINGLE_IMAGE = """
 .. image:: /%s
+    :alt: %s
     :class: sphx-glr-single-img
 """
 


Test patch:
diff --git a/sphinx_gallery/tests/test_full.py b/sphinx_gallery/tests/test_full.py
index d444627..e6cb36e 100644
--- a/sphinx_gallery/tests/test_full.py
+++ b/sphinx_gallery/tests/test_full.py
@@ -24,7 +24,7 @@ from sphinx_gallery.utils import _get_image, scale_image
 
 import pytest
 
-N_TOT = 6
+N_TOT = 7
 N_FAILING = 1
 N_GOOD = N_TOT - N_FAILING
 N_RST = 14 + N_TOT
@@ -550,3 +550,23 @@ def test_rebuild(tmpdir_factory, sphinx_app):
         # mtimes for .ipynb files
         _assert_mtimes(copied_ipy_0, copied_ipy_1,
                        different=('plot_numpy_matplotlib.ipynb'))
+
+
+def test_alt_text(sphinx_app):
+    """Test alt text"""
+    src_dir = sphinx_app.srcdir
+    fname = op.join(src_dir, 'auto_examples', 'plot_matplotlib_alt.rst')
+    assert op.isfile(fname)
+    with codecs.open(fname, 'r', 'utf-8') as fid:
+        rst = fid.read()
+    # suptitle and axes titles
+    assert ':alt: This is a sup title, subplot 1, subplot 2' in rst
+    # multiple titles
+    assert ':alt: Left Title, Center Title, Right Title' in rst
+
+    fname = op.join(src_dir, 'auto_examples', 'plot_numpy_matplotlib.rst')
+    assert op.isfile(fname)
+    with codecs.open(fname, 'r', 'utf-8') as fid:
+        rst = fid.read()
+    # file name when no fig title
+    assert 'plot numpy matplotlib' in rst
diff --git a/sphinx_gallery/tests/test_scrapers.py b/sphinx_gallery/tests/test_scrapers.py
index 8d303c7..f313f76 100644
--- a/sphinx_gallery/tests/test_scrapers.py
+++ b/sphinx_gallery/tests/test_scrapers.py
@@ -175,6 +175,7 @@ def test_figure_rst(ext):
     image_rst = figure_rst(figure_list, '.')
     single_image = """
 .. image:: /sphx_glr_plot_1.{ext}
+    :alt: pl
     :class: sphx-glr-single-img
 """.format(ext=ext)
     assert image_rst == single_image
@@ -188,12 +189,14 @@ def test_figure_rst(ext):
     *
 
       .. image:: /sphx_glr_plot_1.{ext}
-            :class: sphx-glr-multi-img
+          :alt: pl
+          :class: sphx-glr-multi-img
 
     *
 
       .. image:: /second.{ext}
-            :class: sphx-glr-multi-img
+          :alt: pl
+          :class: sphx-glr-multi-img
 """.format(ext=ext)
     assert image_rst == image_list_rst
 
@@ -201,7 +204,7 @@ def test_figure_rst(ext):
     local_img = [os.path.join(os.getcwd(), 'third.' + ext)]
     image_rst = figure_rst(local_img, '.')
 
-    single_image = SINGLE_IMAGE % ("third." + ext)
+    single_image = SINGLE_IMAGE % ("third." + ext, '')
     assert image_rst == single_image
 
 
diff --git a/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py b/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py
new file mode 100644
index 0000000..53eb8a7
--- /dev/null
+++ b/sphinx_gallery/tests/tinybuild/examples/plot_matplotlib_alt.py
@@ -0,0 +1,36 @@
+"""
+======================
+Matplotlib alt text
+======================
+
+This example tests that the alt text is generated correctly for matplotlib
+figures.
+"""
+
+import matplotlib.pyplot as plt
+
+fig, axs = plt.subplots(2, 1, constrained_layout=True)
+axs[0].plot([1, 2, 3])
+axs[0].set_title('subplot 1')
+axs[0].set_xlabel('x label')
+axs[0].set_ylabel('y lab')
+fig.suptitle('This is a sup title')
+
+axs[1].plot([2, 3, 4])
+axs[1].set_title('subplot 2')
+axs[1].set_xlabel('x label')
+axs[1].set_ylabel('y label')
+
+plt.show()
+
+
+# %%
+# Several titles.
+
+plt.plot(range(10))
+
+plt.title('Center Title')
+plt.title('Left Title', loc='left')
+plt.title('Right Title', loc='right')
+
+plt.show()