Skip to content

Commit

Permalink
Update README and package config (#5)
Browse files Browse the repository at this point in the history
  • Loading branch information
yanovs committed Jan 5, 2023
1 parent a46941c commit 5314a79
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 40 deletions.
2 changes: 2 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# See https://github.com/github/linguist/blob/master/docs/overrides.md
*.html linguist-generated
138 changes: 102 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
# leda

Generate static reports with interactive widgets from Jupyter notebooks
Generate static HTML reports with interactive widgets from Jupyter notebooks

[![PyPI version](https://badge.fury.io/py/leda.svg)](https://badge.fury.io/py/leda)
[![PyPI Supported Python Versions](https://img.shields.io/pypi/pyversions/leda.svg)](https://pypi.python.org/pypi/leda/)
[![GitHub Actions (Tests)](https://github.com/ansatzcapital/leda/workflows/Test/badge.svg)](https://github.com/ansatzcapital/leda)

## Installation

`leda` is available on [PyPI](https://pypi.org/project/leda/):

```bash
pip install leda
```

## Quick Start

Generate a static HTML report from a Jupyter notebook:
### Generation

To generate a static HTML report from a Jupyter notebook, run:

```bash
python -m leda /path/to/nb.ipynb --output-dir ./outputs/
Expand All @@ -18,64 +28,115 @@ python -m leda /path/to/nb.ipynb --output-dir ./outputs/ \
-i "abc = 123" -k "other_kernel" --cell-timeout 100
```

This will automatically include formatting tweaks, including, e.g., hiding all input code.
This generation automatically includes tweaks to the notebook to make
the output look more report-like (e.g., hiding all input code)

See the [**static demos**](https://ansatzcapital.github.io/leda/leda/tests/integration/refs/)
being served by GitHub Pages.

See the [**static demos** being served by GitHub Pages](https://ansatzcapital.github.io/leda/leda/tests/integration/refs/).
`leda` is like:
- [`voila`](https://voila.readthedocs.io/en/stable/using.html),
but static, with no need for live kernels
- [`nbconvert`](https://github.com/jupyter/nbconvert)/
[`nbviewer`](https://nbviewer.org/), but with interactive widgets
- [`pretty-jupyter`](https://github.com/JanPalasek/pretty-jupyter),
but with interactive widgets

Think of it like:
- [`voila`](https://voila.readthedocs.io/en/stable/using.html) but static, with no need for live kernels
- [nbconvert](https://github.com/jupyter/nbconvert)/[nbviewer](https://nbviewer.org/) but with interactive widgets
- [pretty-jupyter](https://github.com/JanPalasek/pretty-jupyter) but with interactive widgets
The `-i` (`--inject`) arg is used to inject user code (and set report params)
via a new cell prepended to the notebook during generation.

`-i` (`--inject`) arg is used to inject user code (and set report params) via a new cell prepended to the notebook during generation.
And `--template_name`/`--theme` args allow you to choose between `classic`, `lab` (`light`/`dark`), and `lab_narrow` (`light`/`dark`).
And the `--template_name`/`--theme` args allow you to choose between
`classic`, `lab` (`light`/`dark`), and `lab_narrow` (`light`/`dark`).

**Note**: `leda` assumes that all code is run in a trusted environment, so please be careful.
**Note**: `leda` assumes that all code is run in a trusted environment,
so please be careful.

### Interaction/Widgets

`leda` provides an `%%interact` [magic](https://ipython.readthedocs.io/en/stable/interactive/magics.html)
that makes it easy to create outputs based on widgets, like:
`leda` also provides an `%%interact` [magic](https://ipython.readthedocs.io/en/stable/interactive/magics.html)
that makes it easy to create outputs based on widgets that work in both dynamic
and static modes, e.g.:

```python
# In[ ]:


import leda # Loads `interact` magic when running in Jupyter notebook
import numpy as np
import pandas as pd


# In[ ]:


%%interact column=list("abcdefghij");mult=[1, 2, 3]
df = pd.DataFrame(np.random.RandomState(42).rand(100, 10), columns=list("abcdefghij"))
(df[[column]] * mult).plot(figsize=(15, 8), lw=2, title=f\"column={column!r}, mult={mult}\")
df = pd.DataFrame(
np.random.RandomState(42).rand(100, 10), columns=list("abcdefghij")
)
title = f"column={column!r}, mult={mult}"
(df[[column]] * mult).plot(figsize=(15, 8), lw=2, title=title)
```

There are two types of interact modes: dynamic and static. Dynamic mode is when you're running the Jupyter notebook
live, in which case you will re-compute the cell output every time you select a different `mult`.
There are two types of interact modes: dynamic and static.

In a static mode (using whichever static widget backend is configured), the library will pre-compute
all possible combinations of widget states ([see Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product))
and then render a static HTML report that contains widgets that look and feel like the dynamic widgets
(despite being pre-rendered).
**Dynamic mode** is when you're running the Jupyter notebook
live, in which case you will re-compute the cell output
every time you select a different `mult`. We always use
[`ipywidgets`](https://ipywidgets.readthedocs.io/en/stable/) as the
dynamic widget backend.

In a **static mode** (using whichever static widget backend is configured),
the library will pre-compute all possible combinations of widget outputs
([see Cartesian product](https://en.wikipedia.org/wiki/Cartesian_product))
and then render a static HTML report that contains widgets
that look and feel like the dynamic widgets (despite being pre-rendered).
See below for a list of supported static backends.

### Report Web UI Server

Unlike [`voila`](https://voila.readthedocs.io/en/stable/using.html), because all report output is **static HTML**,
you can stand up a report web UI server that suits your needs very easily. That means:
Unlike [`voila`](https://voila.readthedocs.io/en/stable/using.html),
because all report output is _static HTML_,
you can stand up a report web UI server that suits your needs very easily.
That means:
- It's trivial to set up in many cases.
- It's as scalable as your web server.
- It's as scalable as your web server's ability to distribute static content.
- It's more cost-efficient because there are no runtimes whatsoever.
- You don't have to worry about old versions no longer working due to code or data changes, so the historical
archive of old reports never expire or change or break.
- You don't have to worry about old versions no longer working
due to code or data changes, so the historical
archive of old reports never expires or changes or breaks.

For example, you can generate the report to a file, upload that file to a shared location, and then stand
up a bare-bones `nginx` server to serve the files. (Instead of having a two-step of generation + upload,
you could alternatively implement your own `leda.gen.base.ReportPublisher` and create a generation script of your own).
For example, you can generate the report to a file,
upload that file to a shared location, and then stand
up a bare-bones `nginx` server to serve the files.
Instead of having a two-step process of generation + upload,
you could alternatively implement your own `leda.ReportPublisher`
and create a generation script of your own--or use it as a library
in client script.

Another example is you can simply host a static S3 bucket, enable website hosting and then either use S3 as a web server publically or via locked down S3 endpoint.
Another example is you can simply host a static S3 bucket,
enable website hosting and then either use S3 as a web server
publically or via locked down S3 endpoint.

You could also use [GitHub Pages](https://pages.github.com), much like the [static demos page](https://ansatzcapital.github.io/leda/leda/tests/integration/refs/).
You could also use [GitHub Pages](https://pages.github.com),
much like the [static demos page](https://ansatzcapital.github.io/leda/leda/tests/integration/refs/).

### Params

Reports can be parametrized so that the user can set different values for each report run.
Reports can be parametrized so that the user can set
different values for each report run.

In the notebook, just use `leda.get_param()`:

```python
# In[ ]:


import leda


# In[ ]:


data_id = leda.get_param("data_id", dynamic_default=1, static_default=2)
```

Expand All @@ -89,23 +150,28 @@ python -m leda /path/to/nb.ipynb --output ./outputs/ -i "data_id = 100"

`leda` is built to work with multiple visualization and widget libraries.

Works with these visualization libraries:
It works with these visualization libraries:
- [`matplotlib`](https://matplotlib.org/)
- [`plotly`](https://plotly.com/python/)

With the default dynamic widget library:
- [`ipywidgets`](https://ipywidgets.readthedocs.io/en/stable/)

And with these static widget libraries:
- [`static_ipywidgets`](https://github.com/jakevdp/ipywidgets-static) (vendored and modified)
- [`static_ipywidgets`](https://github.com/jakevdp/ipywidgets-static)
(vendored and modified)
- [`panel`](https://panel.holoviz.org/)

## Testing

See the `requirements-bundle*.txt` for version bundles that we currently test systematically.
See the `requirements-bundle*.txt` for version bundles
that we currently test systematically.

## Known Issues

- There are multiple issues using `matplotlib` with `panel`, including:
- The last widget output is not different from the penultimate one: https://github.com/holoviz/panel/issues/1222
- All the widget outputs show up sequentially, instead of being hidden until chosen. This seems to be a known issue per the [`panel` FAQ](https://panel.holoviz.org/FAQ.html); however, using the example fix provided does not work.
- All the widget outputs show up sequentially,
instead of being hidden until chosen.
This seems to be a known issue per the [`panel` FAQ](https://panel.holoviz.org/FAQ.html);
however, using the example fix provided does not work.
2 changes: 1 addition & 1 deletion leda/interact/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def get_param(
static_default: Any = dataclasses.MISSING,
default: Any = dataclasses.MISSING,
) -> Any:
user_ns = IPython.get_ipython().user_ns
user_ns = IPython.get_ipython().user_ns # pyright: ignore
if name in user_ns:
return user_ns[name]

Expand Down
5 changes: 3 additions & 2 deletions leda/vendor/static_ipywidgets/static_ipywidgets/interact.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,16 @@ def _get_html(
return markdown2.Markdown().convert(obj._repr_markdown_())

ip = IPython.get_ipython()
ip_display_formatter = ip.display_formatter # pyright: ignore

png_rep = ip.display_formatter.formatters["image/png"](obj)
png_rep = ip_display_formatter.formatters["image/png"](obj)
if png_rep is not None:
if isinstance(obj, plt.Figure):
plt.close(obj) # Keep from displaying twice
img_tag = img_manager.add_image(div_name, png_rep, disp=disp)
return img_tag

html_rep = ip.display_formatter.formatters["text/html"](obj)
html_rep = ip_display_formatter.formatters["text/html"](obj)
if html_rep is not None:
return html_rep

Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ python_requires = >=3.7
install_requires =
# See also requirements-bundle*.txt.
# requirements-bundle0 is used as a min constraint.
cached-property == 1.5.2
cached-property >= 1.5.2
ipython >= 7.16.1
ipywidgets >= 7.5.1
markdown2 >= 2.3.9
Expand Down

0 comments on commit 5314a79

Please sign in to comment.