Repository: scipy/scipy

Issue:
DOC: Add interactive examples with jupyterlite-sphinx
### Issue with current documentation:

It would be nice to add some interactivity to SciPy's documentation. This could be done in-browser with  [Jupyterlite](https://jupyterlite.readthedocs.io/en/stable/) running a WASM based kernel like [pyodide](https://pyodide.org/en/stable/). Earlier this year, [Scikit-learn added Jupyterlite buttons](https://github.com/scikit-learn/scikit-learn/pull/25887) to its examples galleries. To see it in action, you can check the "launch lite" button at the bottom of the page [here](https://scikit-learn.org/stable/auto_examples/classification/plot_classifier_comparison.html).

### Idea or request for content:

I've created a Sphinx directive `TryExamples` which was accepted into [jupyterlite-sphinx](https://jupyterlite-sphinx.readthedocs.io/en/latest/), a Sphinx extension that makes it easy to embed Jupyterlite notebooks into Sphinx documentation. You may have seen the [Scientific Python Discourse post](https://discuss.scientific-python.org/t/making-docstring-examples-interactive/812) I made earlier this year. I have an [open PR](https://github.com/jupyterlite/jupyterlite-sphinx/pull/116) which should bring the `TryExamples` directive to a production ready state. I think it's time to start talking about actually deploying this in SciPy's docs.

The extension adds a button to each rendered doctest examples section. Clicking the button will swap the rendered examples in-place with an embedded Jupyterlite notebook. You can see it in action for SciPy 1.11.2's documentation at https://steppi.github.io/scipy.

One thing that needs to be considered before this could be shipped is that we need to take care that the SciPy version being documented is that same as that available on the Jupyterlite kernel used. This means we would either need to restrict the use of the directive to the latest release build which is available in Pyodide, or set up CI builds of SciPy for Pyodide in order to use the directive in dev docs. For now, the use of the directive could be guarded behind an environment variable which is only set for the latest release docs. I'd be curious to know what Scikit-learn does; I wasn't able to figure out from a quick look through their docs.

Scikit-learn gives a disclaimer that the interactive documentation is experimental, may have unexpected behavior, and has long load times. I haven't added anything like this yet, but have thought about adding a little question icon next to each button which displays a tooltip with such a warning.

Feel free to check the examples at https://steppi.github.io/scipy and let me know if there's anything that you think could be improved. The button style, position, and text is easily configurable if you think any of this should be changed.

### Additional context (e.g. screenshots, GIFs)

![image](https://github.com/scipy/scipy/assets/1953382/c28c2835-285e-4d03-a0d8-3ff9c91f29ab)

after clicking the button...

![image](https://github.com/scipy/scipy/assets/1953382/60649378-c67c-415e-a903-702670e70238)



Discussion:
How do we prevent folks doing nasty stuff like importing `urllib` then spamming around or similar crypto-chad silliness? I don't know what the current way of frontloading such stuff, but is this in general a sandboxed operation? 
> How do we prevent folks doing nasty stuff like importing `urllib` then spamming around or similar crypto-chad silliness? I don't know what the current way of frontloading such stuff, but is this in general a sandboxed operation?

Great question. It runs in the browser so they’d just be running their spammy stuff locally. 
@dschmitz89 I think you were interested in this
From quickly inspecting the scikit-learn docs, it looks like the scikit-learn version you get in the jupyter-lite environment is 1.3.1 no matter which docs version you are looking at (the stable version is 1.3.2 and dev is 1.5.0dev). It looks like there was a brief discussion about this here: https://github.com/scikit-learn/scikit-learn/issues/26763 and a description of the provisional set up here: https://github.com/scikit-learn/scikit-learn/pull/25887#issuecomment-1514714120

In any case, very excited to see this. Do you have any documentation (even in draft) on what the process is to build this and what would change in docs build, release process etc for this to work in scipy?
Thanks @melissawm! That's really helpful to see what sklearn is doing; I guess I didn't look closely enough.

> In any case, very excited to see this. Do you have any documentation (even in draft) on what the process is to build this and what would change in docs build, release process etc for this to work in scipy?

Here's the [documentation for the TryExamples directive](https://jupyterlite-sphinx.readthedocs.io/en/latest/directives/try_examples.html). Modulo getting the docs and Pyodide SciPy versions synced up, set up is as simple as adding the `jupyterlite-sphinx` extension to the doc requirements, adding it to the list of extensions in the docs `conf.py`

```python
extensions = [
    'sphinx.ext.autodoc',
    'sphinx.ext.autosummary',
    'sphinx.ext.coverage',
    'sphinx.ext.mathjax',
    'sphinx.ext.intersphinx',
    'numpydoc',
    'sphinx_design',
    'scipyoptdoc',
    'doi_role',
    'matplotlib.sphinxext.plot_directive',
    'myst_nb',
    'jupyterlite_sphinx',
]
```
and adding configuration for it in `conf.py`

```python
# -----------------------------------------------------------------------------
# Interactive examples with jupyterlite-sphinx
# -----------------------------------------------------------------------------
global_enable_try_examples = os.environ.get("GLOBAL_ENABLE_TRY_EXAMPLES")
try_examples_global_button_css = """
color: white;
background-color: #0054a6;
border: none;
padding: 5px 10px;
border-radius: 10px;
margin-bottom: 5px;
box-shadow: 0 2px 5px rgba(108,108,108,0.2);
"""
try_examples_global_button_hover_css = """
background-color: #0066cc;
transform: scale(1.02);
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
cursor: pointer;
"""

try_examples_global_button_text = "Try it in your browser!"
try_examples_global_min_height = "200px"
```

the above is how I have it set up for what's hosted at https://steppi.github.io/scipy. Whether or not interactive docs are enabled is determined by the environment variable `GLOBAL_ENABLE_TRY_EXAMPLES`. I still haven't looked into what it will take to ship up to date wheels for SciPy on pyodide and use these in our docs builds. For now, I figured we could just enable interactivity for the release docs.

> For now, I figured we could just enable interactivity for the release docs.

+1 for doing that first. Most users look at the docs for the latest released version. 

One question I have there is how long we expect things to remain stable/installable with JupyterLite. E.g., are the numpy/scipy versions pinned, so things don't move from under the stable docs if new versions land in Pyodide? If so, we're probably good, because we have so few dependencies. For other projects that depend on numpy/scipy/pandas/etc. like scikit-learn, that may become more shaky perhaps - do you know what scikit-learn's experience there is so far?

Other topic: I did a bit more playing around and while overall things seem to work smoothly, it'd be useful to increase the minimum version size that the embedded window with the notebook uses. This is too few vertical lines when the docstring example is short, making it hard to read and use (that's for `linalg.solve`):

<img width="505" alt="image" src="https://github.com/scipy/scipy/assets/98330/d31e5719-36a3-4f2a-a31a-18faa4806368">

I'd suggest that double that vertical size would be a reasonable minimum.
Thanks Ralf.

> One question I have there is how long we expect things to remain stable/installable with JupyterLite. E.g., are the numpy/scipy versions pinned, so things don't move from under the stable docs if new versions land in Pyodide? If so, we're probably good, because we have so few dependencies. For other projects that depend on numpy/scipy/pandas/etc. like scikit-learn, that may become more shaky perhaps - do you know what scikit-learn's experience there is so far?

My understanding is that if we pin `jupyterlite-pyodide-kernel`, then the included package versions will remain pinned too. That is, each version of `jupyterlite-pyodide-kernel` comes with a fixed set of package versions.

> Other topic: I did a bit more playing around and while overall things seem to work smoothly, it'd be useful to increase the minimum version size that the embedded window with the notebook uses. This is too few vertical lines when the docstring example is short, making it hard to read and use (that's for `linalg.solve`):

Agreed, I'll change that.

I guess I'll make a PR setting everything up with the interactive examples disabled, and we can enable it for the release docs once `jupyterlite-pyodide-kernel` ships with SciPy 1.12, since that will imminently be the new SciPy release.
> I guess I'll make a PR setting everything up with the interactive examples disabled, and we can enable it for the release docs once `jupyterlite-pyodide-kernel` ships with SciPy 1.12

That looks like a switch to be used at doc build time, is that right? If so - rebuilding the docs is a bit painful. Could we have a switch that allows enabling/disabling the _Try it ..._ buttons in a single place (e.g. a separate file in the root of the doc build, that contains a boolean value based on which the buttons are shown or hidden on all pages)? That would also allow us to easily turn off the buttons in case there would be a breakage in JupyterLite or a security issue.
> > I guess I'll make a PR setting everything up with the interactive examples disabled, and we can enable it for the release docs once `jupyterlite-pyodide-kernel` ships with SciPy 1.12
> 
> That looks like a switch to be used at doc build time, is that right? If so - rebuilding the docs is a bit painful. Could we have a switch that allows enabling/disabling the _Try it ..._ buttons in a single place (e.g. a separate file in the root of the doc build, that contains a boolean value based on which the buttons are shown or hidden on all pages)? That would also allow us to easily turn off the buttons in case there would be a breakage in JupyterLite or a security issue.

Ok, if I understand correctly, you're asking if we could have a switch that could be used to enable or disable the interactive examples without having to rebuild the docs? Yeah, I think that should be straightforward. `jupyterlite_sphinx.js` could check for the presence of the file and just hide the button if the flag is set to `false`. Sounds like a good idea, I'll work on that and PR to jupyterlite-sphinx.

Resolving patch:
diff --git a/doc/source/conf.py b/doc/source/conf.py
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -52,6 +52,7 @@
     'doi_role',
     'matplotlib.sphinxext.plot_directive',
     'myst_nb',
+    'jupyterlite_sphinx',
 ]
 
 
@@ -249,6 +250,7 @@
 
 html_css_files = [
     "scipy.css",
+    "try_examples.css",
 ]
 
 # html_additional_pages = {
@@ -389,6 +391,22 @@
 # -----------------------------------------------------------------------------
 
 nb_execution_mode = "auto"
+# Ignore notebooks generated by jupyterlite-sphinx for interactive examples.
+nb_execution_excludepatterns = ["_contents/*.ipynb"]
+
+#------------------------------------------------------------------------------
+# Interactive examples with jupyterlite-sphinx
+#------------------------------------------------------------------------------
+global_enable_try_examples = True
+try_examples_global_button_text = "Try it in your browser!"
+try_examples_global_warning_text = (
+    "SciPy's interactive examples with Jupyterlite are experimental and may"
+    " not always work as expected. Execution of cells containing imports may"
+    " result in large downloads (up to 60MB of content for the first import"
+    " from SciPy). Load times when importing from SciPy may take roughly 10-20"
+    " seconds. If you notice any problems, feel free to open an"
+    " [issue](https://github.com/scipy/scipy/issues/new/choose)."
+)
 
 # -----------------------------------------------------------------------------
 # Source code links


Test patch: