From 6222017b94d663e803b743968e7273ea4a451612 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 22 Sep 2021 09:51:25 +0100 Subject: [PATCH] V2.0.0 (#58) * WIP * Update README * Update README * WIP * WIP * WIP * Update docstrings etc * Update documentation * Fix bugs and typos * Update documentation * Update documentation * Fix bug --- .flake8 | 8 +- .travis.yml | 2 +- Makefile | 2 +- README.md | 123 +- docs/01-getting-started/quickstart.md | 2 +- docs/02-user-guide/customisation.md | 9 + docs/02-user-guide/examples.md | 49 +- docs/02-user-guide/general-usage-old.md | 75 + docs/02-user-guide/quick-reference-old.md | 81 + docs/02-user-guide/quick-reference.md | 79 + docs/03-api-reference/layout.md | 8 + docs/03-api-reference/options.md | 11 + docs/03-api-reference/publish.md | 8 +- docs/04-about/release-notes.md | 18 +- docs/examples/getting-started.ipynb | 931 ++ docs/examples/interactive-plots.html | 162 +- docs/examples/interactive-plots.ipynb | 12483 +------------------- docs/examples/interactive-plots.pdf | Bin 105339 -> 93200 bytes docs/examples/iris-report.html | 1175 +- docs/examples/iris-report.ipynb | 1430 +-- docs/examples/iris-report.pdf | Bin 256840 -> 256208 bytes docs/examples/my-image.png | Bin 0 -> 12638 bytes esparto/__init__.py | 92 +- esparto/_adaptors.py | 12 +- esparto/_cdnlinks.py | 44 + esparto/_content.py | 188 +- esparto/_contentdeps.py | 17 +- esparto/_layout.py | 590 +- esparto/_options.py | 173 +- esparto/_publish.py | 68 +- esparto/_utils.py | 37 +- esparto/resources/css/esparto.css | 12 +- esparto/resources/jinja/base.html.jinja | 10 +- mkdocs.yml | 13 +- poetry.lock | 817 +- pyproject.toml | 11 +- tests/conftest.py | 4 + tests/test_adaptors.py | 6 + tests/test_content.py | 26 + tests/test_layout.py | 86 +- tests/test_publish.py | 37 +- tests/test_utils.py | 13 + 42 files changed, 4141 insertions(+), 14771 deletions(-) create mode 100644 docs/02-user-guide/customisation.md create mode 100644 docs/02-user-guide/general-usage-old.md create mode 100644 docs/02-user-guide/quick-reference-old.md create mode 100644 docs/02-user-guide/quick-reference.md create mode 100644 docs/03-api-reference/options.md create mode 100644 docs/examples/getting-started.ipynb create mode 100644 docs/examples/my-image.png create mode 100644 esparto/_cdnlinks.py create mode 100644 tests/test_utils.py diff --git a/.flake8 b/.flake8 index be1b07e..14e338a 100644 --- a/.flake8 +++ b/.flake8 @@ -1,5 +1,9 @@ [flake8] max-line-length = 120 exclude = scratch.*, docs/ -ignore = W503 # line break before binary operator -per-file-ignores = __init__.py:E402, F401 +ignore = + W503, # line break before binary operator + E203, # whitespace before ':' +per-file-ignores = + __init__.py:E402, F401 + _cdnlinks.py:E501 diff --git a/.travis.yml b/.travis.yml index ce24e8f..808810a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ python: env: - EXTRA_INSTALLS="" - - EXTRA_INSTALLS="ipython pandas matplotlib bokeh plotly kaleido weasyprint beautifulsoup4 html5lib" + - EXTRA_INSTALLS="ipython pandas matplotlib bokeh plotly kaleido weasyprint<53 beautifulsoup4 html5lib" install: - pip install . pytest coverage $EXTRA_INSTALLS diff --git a/Makefile b/Makefile index 5e772c8..59cb7e6 100644 --- a/Makefile +++ b/Makefile @@ -72,7 +72,7 @@ test-all: ## run tests on every Python version with tox python -m tests.check_package_version coverage: ## check code coverage quickly with the default Python - coverage run --source esparto -m pytest + -coverage run --source esparto -m pytest coverage report -m coverage html # $(BROWSER) htmlcov/index.html diff --git a/README.md b/README.md index f13a21f..36291a5 100644 --- a/README.md +++ b/README.md @@ -10,35 +10,43 @@ esparto ## Introduction -`esparto` is a simple HTML and PDF document generator for Python. -The library takes a fully Pythonic approach to defining documents, -allowing iterative building and modification of the page and its contents. +**esparto** is a Python package for building shareable reports with content +from popular data analysis libraries. +With just a few lines of code, **esparto** turns DataFrames, plots, and +Markdown into an interactive webpage or PDF document. +Documents produced by **esparto** are completely portable - no backend server +is required - and entirely customisable using CSS and Jinja templating. +All content dependencies are declared inline or loaded via a CDN, meaning your +reports can be shared by email, hosted on a standard http server, or made +available as static pages as-is. -### Example Use Cases -* Automated MI reporting -* Collating and sharing data visualisations -* ML model performance and evaluation documents -* Designing simple web pages + +## Basic Usage +```python +import esparto as es +page = es.Page(title="My Report") +page["Data Analysis"] = (pandas_dataframe, plotly_figure) +page.save_html("my-report.html") +``` ## Main Features -* Lightweight API -* Jupyter Notebook support -* Output self-contained HTML and PDF files -* Responsive layout from [Bootstrap](https://getbootstrap.com/) -* No CSS or HTML required -* Implicit conversion for: +* Automatic and adaptive layout +* Customisable with CSS or Jinja +* Jupyter Notebook friendly +* Output as HTML or PDF +* Built-in adaptors for: * Markdown * Images - * Pandas DataFrames - * Matplotlib - * Bokeh - * Plotly + * [Pandas DataFrames][Pandas] + * [Matplotlib][Matplotlib] + * [Bokeh][Bokeh] + * [Plotly][Plotly] ## Installation -`esparto` is available from PyPI: +**esparto** is available from PyPI: ```bash pip install esparto ``` @@ -54,7 +62,8 @@ pip install weasyprint * [jinja2](https://palletsprojects.com/p/jinja/) * [markdown](https://python-markdown.github.io/) * [Pillow](https://python-pillow.org/) -* [weasyprint](https://weasyprint.org/) _(optional - for PDF output)_ +* [PyYAML](https://pyyaml.org/) +* [weasyprint](https://weasyprint.org/) _(optional - required for PDF output)_ ## License @@ -64,69 +73,27 @@ pip install weasyprint ## Documentation Full documentation and examples are available at [domvwt.github.io/esparto/](https://domvwt.github.io/esparto/). - -## Basic Usage -```python -import esparto as es - -# Instantiating a Page -page = es.Page(title="Research") - -# Page layout hierarchy: -# Page -> Section -> Row -> Column -> Content - -# Add or update content -# Keys are used as titles -page["Introduction"]["Part One"]["Item A"] = "./text/content.md" -page["Introduction"]["Part One"]["Item B"] = "./pictures/image1.jpg" - -# Add content without a title -page["Introduction"]["Part One"][""] = "Hello, Wolrd!" - -# Replace child at index - useful if no title given -page["Introduction"]["Part One"][-1] = "Hello, World!" - -# Set content and return input object -# Useful in Jupyter Notebook as it will be displayed in cell output -page["Methodology"]["Part One"]["Item A"] << "dolor sit amet" -# >>> "dolor sit amet" - -# Set content and return new layout -page["Methodology"]["Part Two"]["Item B"] >> "foobar" -# >>> {'Item B': ['Markdown']} - -# Show document structure -page.tree() -# >>> {'Research': [{'Introduction': [{'Part One': [{'Item A': ['Markdown']}, -# {'Item B': ['Image']}]}]}, -# {'Methodology': [{'Part One': [{'Item A': ['Markdown']}]}, -# {'Part Two': [{'Item A': ['Markdown']}]}]}]} - -# Remove content -del page["Methodology"]["Part One"]["Item A"] -del page.methodology.part_two.item_b - -# Access existing content as an attribute -page.introduction.part_one.item_a = "./pictures/image2.jpg" -page.introduction.part_one.tree() -# >>> {'Part One': [{'Item A': ['Image']}, -# {'Item B': ['Image']}, -# {'Column 2': ['Markdown']}]} - -# Save the document -page.save_html("my-page.html") -page.save_pdf("my-page.pdf") -``` +## Contributions, Issues, and Requests +All feedback and contributions are welcome - please raise an issue or pull request on [GitHub][GitHub]. -## Example Output -Iris Report - [HTML](https://domvwt.github.io/esparto/examples/iris-report.html) | +## Examples +Iris Report - [Webpage](https://domvwt.github.io/esparto/examples/iris-report.html) | [PDF](https://domvwt.github.io/esparto/examples/iris-report.pdf) -Bokeh and Plotly - [HTML](https://domvwt.github.io/esparto/examples/interactive-plots.html) | +Bokeh and Plotly - [Webpage](https://domvwt.github.io/esparto/examples/interactive-plots.html) | [PDF](https://domvwt.github.io/esparto/examples/interactive-plots.pdf)
- +

+example page +

+ + +[Bootstrap]: https://getbootstrap.com/docs/4.6/getting-started/introduction/ +[Pandas]: https://pandas.pydata.org/ +[Matplotlib]: https://matplotlib.org/ +[Bokeh]: https://docs.bokeh.org/en/latest/index.html +[Plotly]: https://plotly.com/ +[GitHub]: https://github.com/domvwt/esparto diff --git a/docs/01-getting-started/quickstart.md b/docs/01-getting-started/quickstart.md index 7fb68ed..1ef1496 100644 --- a/docs/01-getting-started/quickstart.md +++ b/docs/01-getting-started/quickstart.md @@ -37,7 +37,7 @@ my_page.save_html("esparto-quick.html") ```
-The rendered HTML document: +The rendered web document: diff --git a/docs/02-user-guide/customisation.md b/docs/02-user-guide/customisation.md new file mode 100644 index 0000000..4f82c34 --- /dev/null +++ b/docs/02-user-guide/customisation.md @@ -0,0 +1,9 @@ +### Jinja + +### CSS + +#### Bootstrap Themes + +### Layout Classes + +### Content Classes diff --git a/docs/02-user-guide/examples.md b/docs/02-user-guide/examples.md index 23ec527..3d4237a 100644 --- a/docs/02-user-guide/examples.md +++ b/docs/02-user-guide/examples.md @@ -1,9 +1,23 @@ -# Examples +# Guides These examples demonstrate recommended ways of working with `esparto`. -Note that Jupyter Notebooks do not preserve the formatting of rendered -content between sessions - be sure to re-run the examples in order to -view the output as intended. + + +## Getting Started + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/domvwt/esparto/blob/main/docs/examples/getting-started.ipynb) +[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/domvwt/esparto/main?filepath=docs%2Fexamples%2Fgetting-started.ipynb) +[![GitHub](https://img.shields.io/badge/view%20on-GitHub-lightgrey)](https://github.com/domvwt/esparto/blob/main/docs/examples/getting-started.ipynb) + +A guided tour of the `esparto` API. This notebook covers: + +* Working with different Content types +* Layout and formatting +* Page options +* Saving your work + +--- + ## Data Analysis @@ -11,43 +25,32 @@ view the output as intended. [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/domvwt/esparto/main?filepath=docs%2Fexamples%2Firis-report.ipynb) [![GitHub](https://img.shields.io/badge/view%20on-GitHub-lightgrey)](https://github.com/domvwt/esparto/blob/main/docs/examples/iris-report.ipynb) -The iris dataset is one of the most well known datasets in statistics and -data science. This notebook shows how we can put together a simple -data analysis report in `esparto`. +This notebook shows how we can put together a simple data analysis in `esparto`. This example covers: -* Text content with markdown formatting +* Text content with Markdown formatting * Including images from files * Converting a Pandas DataFrame to a table * Adding plots from Matplotlib and Seaborn -Output: [HTML](../examples/iris-report.html) | [PDF](../examples/iris-report.pdf) +Output: [Webpage](../examples/iris-report.html) | [PDF](../examples/iris-report.pdf) ---- - -## Interactive Plotting +## Interactive Plots [![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/domvwt/esparto/blob/main/docs/examples/interactive-plots.ipynb) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/domvwt/esparto/main?filepath=docs%2Fexamples%2Finteractive-plots.ipynb) [![GitHub](https://img.shields.io/badge/view%20on-GitHub-lightgrey)](https://github.com/domvwt/esparto/blob/main/docs/examples/interactive-plots.ipynb) -The [pandas-bokeh](https://github.com/PatrikHlobil/Pandas-Bokeh) library -offers convenient functions for producing interactive Bokeh plots with -few lines of code. - -With the [Plotly backend for Pandas](https://plotly.com/python/pandas-backend/) -we can access the Plotly Express API directly from the `.plot()` method of -any DataFrame or Series. - -This notebook shows basic examples from each library: +This notebook shows basic examples from interactive plotting libraries: -* Interactive plotting with Bokeh and Plotly -* Adding interactive content to the page +* Bokeh +* Plotly -Output: [HTML](../examples/interactive-plots.html) | [PDF](../examples/interactive-plots.pdf) +Output: [Webpage](../examples/interactive-plots.html) | [PDF](../examples/interactive-plots.pdf) !!! note PDF output is not officially supported for `Bokeh` at this time. diff --git a/docs/02-user-guide/general-usage-old.md b/docs/02-user-guide/general-usage-old.md new file mode 100644 index 0000000..cc6533c --- /dev/null +++ b/docs/02-user-guide/general-usage-old.md @@ -0,0 +1,75 @@ +## Offline Mode +When working in an environment with no internet connection it is necessary to +use inline content dependencies rather than the preferred Content Distribution Network (CDN). + + +Offline mode can be activated by changing the relevant `esparto.options` attribute: +```python +import esparto as es + +es.options.offline_mode = True +``` + +## Matplotlib Output +To produce sharp and scalable images, esparto defaults to SVG format for +static plots. +This can cause a significant drain on resources when plotting a high number +of data points and so PNG format may be preferred. + +PNG format can be selected for all Matplotlib plots: +```python +es.options.matplotlib_output_format = "png" +``` + +Or configured on a case by case basis: +```python +fig = df.plot() +esparto_fig = es.FigureMpl(fig, output_format="png") +``` + +Options provided directly to `FigureMpl` will override the global configuration. + +## PDF Output + +### From the API +Saving a page to PDF is achieved through the API by calling the `.save_pdf()` +method from a `Page` object: + +```python +import esparto as es + +my_page = es.Page(title="My Page") +my_page += "image.jpg" +my_page.save_pdf("my-page.pdf) +``` + +In order to render plots for PDF output, they must be rendered to SVG. While +this leads to consistent and attractive results for Matplotlib figures, it is +less predictable and requires additional system configuration for Bokeh and +Plotly objects. + +#### Plotly +The preferred approach with Plotly is to use the Kaleido library, which is installable +with pip: +```bash +pip install kaleido +``` +Esparto will automatically handle the conversion, provided Kaleido is available. + +Make sure to inspect results for unusual cropping and other artifacts. + +#### Bokeh +The approach taken by Bokeh is to use a browser and webdriver combination. +I have not been able to make this work during testing but the functionality +has been retained in esparto should you have more luck with it. + +See the Bokeh documenation on [additional dependencies for exporting plots.](https://docs.bokeh.org/en/latest/docs/user_guide/export.html#additional-dependencies) + +Conversion should be handled by esparto, provided the Bokeh dependencies +are satisfied. + +### Saving from a Browser +Alternatively, it is possible to save any HTML page as a PDF through the print +menu in your web browser. This method should work with all content types. + +
diff --git a/docs/02-user-guide/quick-reference-old.md b/docs/02-user-guide/quick-reference-old.md new file mode 100644 index 0000000..ba912c3 --- /dev/null +++ b/docs/02-user-guide/quick-reference-old.md @@ -0,0 +1,81 @@ +# Quick Reference + + +### Create a New Page +All pages start with a `Page` object. Additional options are available for +including a Table of Contents and a brand name in the header. +```python +import esparto as es +page = es.Page(title="My Page", navbrand="esparto", table_of_contents=True) +``` +### Page Layout +Pages items always follow the same hierarchy: + +`Page` -> `Section` -> `Row` -> `Column` -> `Content` + +If content is passed without explicitly defining the hierarcy it will be automatically inferred. + +### Add Content +`Page` elements can be created and retrieved by title or by index. Elements can also be retrieved as attributes if they already exist. +If new content is passed to an index it will either overwrite the content at that position or be appended to the parent element's child list. + +```python +page["Section Title"]["Row Title"]["Column Title"] = content # get element by title +page.section_title.row_title.column_title = content # get element by attribute name (if it exists) +page[0][1][2] = content # get element by index +page[0][1][-1] = content # overwrite the last item +page["Section Title"] = {"column title": column content} # use a dict to pass a column title + +page["Section Title"] << content # add content and return original object +page["Section Title"] >> content # add content and return esparto element + +page["Section Title"][0] = (content_a, content_b) # use a tuple to place content in the same row +page["Section Title"]["Row Title"][0] = (content_a, content_b) # or the same column +``` + +_NOTE: +When an item is added to the page it will either overwrite an existing item or be appended to the end of the parent element child list, regardless of the given index._ + +### Remove content +```python +del page["Section Title"]["Row Title"]["Column Title"] # by title +del page.section_title.row_title.column_title # by attribute name +del page[0][0][0] # by index +``` +### View the Page +```python +page.tree() # print the page structure +page.display() # render the page in Jupyter cell output +``` + +### Save the Page +```python +page.save_html("my-page.html") +page.save_pdf("my-page.pdf") +``` + +### Using Cards +`Card` objects are a useful way of grouping related content items. They can be added explicitly as content: +```python +page["Section Title"][0][0] = es.Card(title="Card Title", children=[content]) +``` +Or generated implicitly in a `CardSection`: +```python +page["Section Title"] = es.CardSection() +page["Section Title"][0][0] = {"Card Title": content} +``` + +### Spacers and PageBreaks +A `Spacer` is used to fill add empty space to a `Row`. The `PageBreak` object is used to force a page-break in printed or PDF output. +```python +page["Section Title"]["Row Title"] = content, es.Spacer() # content will share row space equally with Spacer +page["Section Title"] += es.PageBreak() # add a page break at the end of the section +``` + +### Config Options +There are several options available for configuring the behaviour and appearance of the `Page`. Call `help` on the `options` object for more information or check the relevant documentation. +```python +help(es.options) +# or +es.options? +``` diff --git a/docs/02-user-guide/quick-reference.md b/docs/02-user-guide/quick-reference.md new file mode 100644 index 0000000..28e855c --- /dev/null +++ b/docs/02-user-guide/quick-reference.md @@ -0,0 +1,79 @@ +# Quick Start + +## Create a Page +```python +page = es.Page(title="Page Title") +page[0] = "Page Content" +page.tree() +``` +``` +{'Page Title': [{'Section 0': [{'Row 0': [{'Column 0': ['Markdown']}]}]}]} +``` + +## Add Content +### Define Rows and Columns +```python +page = es.Page("Page Title") +page["Section One"]["Row One"]["Column One"] = "Some content" +page["Section One"]["Row One"]["Column Two"] = "More content" +page.tree() +``` +``` +{'Page Title': [{'Section One': [{'Row One': [{'Column One': ['Markdown']}, + {'Column Two': ['Markdown']}]}]}]} +``` + +### Define multiple Columns as a tuple of dicts +```python +page = es.Page("Page Title") +page["Section One"]["Row One"] = ( + {"Column One": "Some content"}, + {"Column Two": "More content"} +) +page.tree() +``` +``` +{'Page Title': [{'Section One': [{'Row One': [{'Column One': ['Markdown']}, + {'Column Two': ['Markdown']}]}]}]} +``` + +## Update Content +### Access existing content via Indexing or as Attributes +```python +page["Section One"]["Row One"]["Column One"] = image_01 +page.section_one.row_one.column_two = image_02 +page.section_one.tree() +``` +``` +{'Page Title': [{'Section One': [{'Row One': [{'Column One': ['Image']}, + {'Column Two': ['Image']}]}]}]} +``` + +## Delete Content +### Delete the last Column +```python +del page.section_one.row_one[-1] +``` +``` +{'Page Title': [{'Section One': [{'Row One': [{'Column One': ['Image']}]}]}]} +``` +### Delete a named Column +```python +del page.section_one.row_one.column_two +page.tree() +``` +``` +{'Page Title': [{'Section One': [{'Row One': [{'Column One': ['Image']}]}]}]} +``` + +## Save the Document +### As a webpage +```python +my_page.save_html("my-esparto-doc.html") +``` +### As a PDF +```python +my_page.save_pdf("my-esparto-doc.pdf") +``` + +
diff --git a/docs/03-api-reference/layout.md b/docs/03-api-reference/layout.md index 8346d73..4a429d3 100644 --- a/docs/03-api-reference/layout.md +++ b/docs/03-api-reference/layout.md @@ -18,10 +18,18 @@ ## ::: esparto._layout.Section +## ::: esparto._layout.CardSection + ## ::: esparto._layout.Row +## ::: esparto._layout.CardRow + +## ::: esparto._layout.CardRowEqual + ## ::: esparto._layout.Column +## ::: esparto._layout.Card + ## ::: esparto._layout.Spacer ## ::: esparto._layout.PageBreak diff --git a/docs/03-api-reference/options.md b/docs/03-api-reference/options.md new file mode 100644 index 0000000..19057e3 --- /dev/null +++ b/docs/03-api-reference/options.md @@ -0,0 +1,11 @@ +# esparto._options + +## ::: esparto._options.ConfigOptions + +## ::: esparto._options.MatplotlibOptions + +## ::: esparto._options.PlotlyOptions + +## ::: esparto._options.BokehOptions + +
diff --git a/docs/03-api-reference/publish.md b/docs/03-api-reference/publish.md index 143f65c..12ab1b9 100644 --- a/docs/03-api-reference/publish.md +++ b/docs/03-api-reference/publish.md @@ -9,7 +9,7 @@ # Create a new Page page = es.Page(title="My New Page") - # Publish the document to an HTML file: + # Publish the page to an HTML file: page.save_html("my-page.html") # Or as a PDF: @@ -17,10 +17,10 @@ ``` -### ::: esparto._publish.publish_html +## ::: esparto._publish.publish_html -### ::: esparto._publish.publish_pdf +## ::: esparto._publish.publish_pdf -### ::: esparto._publish.nb_display +## ::: esparto._publish.nb_display
diff --git a/docs/04-about/release-notes.md b/docs/04-about/release-notes.md index c78531e..2472b88 100644 --- a/docs/04-about/release-notes.md +++ b/docs/04-about/release-notes.md @@ -1,6 +1,20 @@ Release Notes ============= +2.0.0 (2021-09-19) +------------------ +- New Features + - Links to Bootswatch CDN for page themes + - Reorganise and add options to `esparto.options` + - Table of Contents generator for Page element + - Save and Load config options + - Define Columns and Cards as dict of {"title": content} + - Add or replace Content by positional index +- New Layout Classes + - CardSection: Section with Cards as the default Content container + - CardRow: Row of Cards + - CardRowEqual: Row of equal width cards + 1.3.0 (2021-07-19) ------------------ - New Layout class @@ -15,7 +29,7 @@ Release Notes 1.2.0 (2021-06-28) ------------------ -- Implicitly read markdown text files +- Implicitly read Markdown text files 1.1.0 (2021-06-18) @@ -56,7 +70,7 @@ Release Notes 0.2.4 (2021-05-04) ------------------ -- Fix bug corrupting document titles +- Fix bug corrupting page titles - Lazy load the content dependency dict - Remove unused code diff --git a/docs/examples/getting-started.ipynb b/docs/examples/getting-started.ipynb new file mode 100644 index 0000000..228984d --- /dev/null +++ b/docs/examples/getting-started.ipynb @@ -0,0 +1,931 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "sixth-acrobat", + "metadata": {}, + "source": [ + "# Getting Started Guide" + ] + }, + { + "cell_type": "markdown", + "id": "72210dc1", + "metadata": { + "ExecuteTime": { + "start_time": "2021-09-21T19:58:43.373Z" + } + }, + "source": [ + "In this notebook we demonstrate how to build and design a page with **esparto**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b946416f", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:00.994747Z", + "start_time": "2021-09-21T21:39:55.729695Z" + } + }, + "outputs": [], + "source": [ + "# Environment setup\n", + "import os\n", + "!pip install -Uqq esparto\n", + "if os.environ.get(\"BINDER_SERVICE_HOST\"):\n", + " !pip install -Uqq pandas matplotlib seaborn" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "biological-cruise", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:03.581615Z", + "start_time": "2021-09-21T21:40:00.998674Z" + } + }, + "outputs": [], + "source": [ + "import esparto as es\n", + "import pandas as pd\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from pathlib import Path" + ] + }, + { + "cell_type": "markdown", + "id": "df2ff417", + "metadata": {}, + "source": [ + "## Page Creation" + ] + }, + { + "cell_type": "markdown", + "id": "92968a2a", + "metadata": {}, + "source": [ + "All pages start with a Page object. When a Page is initialised we are allowed to specify a page title, which will \n", + "appear at the top of the page, a navbrand which will be displayed in the header, and several other options which are\n", + "detailed in the documentation.\n", + "\n", + "\n", + "Creating the Page::" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a12076b1", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:03.594658Z", + "start_time": "2021-09-21T21:40:03.586730Z" + } + }, + "outputs": [], + "source": [ + "page = es.Page(title=\"Page Title\")" + ] + }, + { + "cell_type": "markdown", + "id": "binding-medicaid", + "metadata": {}, + "source": [ + "## Content Types" + ] + }, + { + "cell_type": "markdown", + "id": "c65988c4", + "metadata": {}, + "source": [ + "The following content types are supported:\n", + "\n", + "* Markdown Text\n", + "* Images\n", + "* Pandas DataFrames\n", + "* Plots from Matplotlib, Plotly, and Bokeh\n", + "\n", + "When a content object is added to the page it is automatically converted to the matching Esparto Content class.\n", + "Additional options may be chosen by explicitly instantiating the object although default settings should be suitable\n", + "for most scenarios." + ] + }, + { + "cell_type": "markdown", + "id": "necessary-midnight", + "metadata": {}, + "source": [ + "### Markdown" + ] + }, + { + "cell_type": "markdown", + "id": "1e051ebe", + "metadata": {}, + "source": [ + "Creating Markdown content from a string:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cordless-jungle", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:03.670702Z", + "start_time": "2021-09-21T21:40:03.600170Z" + } + }, + "outputs": [], + "source": [ + "page = es.Page()\n", + "markdown_text = \"\"\"\n", + "**Markdown** is a *lightweight markup language* for creating formatted text using a plain-text editor. \n", + "**John Gruber** and **Aaron Swartz** created Markdown in 2004 as a markup language that is appealing to human \n", + "readers in its source code form. Markdown is widely used in:\n", + "\n", + "* blogging\n", + "* instant messaging\n", + "* online forums \n", + "* collaborative software \n", + "* documentation pages\n", + "* readme files\n", + "\n", + "----\n", + "\n", + "*From Wikipedia:* [*Markdown*](https://en.wikipedia.org/wiki/Markdown)\n", + "\"\"\"\n", + "page[0] = es.Markdown(markdown_text)\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "863efdc2", + "metadata": {}, + "source": [ + "Reading the same Markdown from a text file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f7afcf5c", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:03.731952Z", + "start_time": "2021-09-21T21:40:03.675106Z" + } + }, + "outputs": [], + "source": [ + "Path(\"markdown.md\").write_text(markdown_text)\n", + "page[0] = \"markdown.md\"\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "narrow-differential", + "metadata": {}, + "source": [ + "### Images" + ] + }, + { + "cell_type": "markdown", + "id": "3b8b5a9e", + "metadata": {}, + "source": [ + "Reading an image from a file path:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "growing-saver", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:03.753250Z", + "start_time": "2021-09-21T21:40:03.738292Z" + } + }, + "outputs": [], + "source": [ + "image_path = \"my-image.png\"\n", + "page[0][0][0] = image_path\n", + "page.tree()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dbf0f88", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.004724Z", + "start_time": "2021-09-21T21:40:03.759832Z" + } + }, + "outputs": [], + "source": [ + "image_credit = \"Photo by Benjamin Voros for Unsplash\"\n", + "page[0][0][1] = es.Image(image_path, caption=image_credit)\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "silver-insert", + "metadata": {}, + "source": [ + "### Pandas DataFrames" + ] + }, + { + "cell_type": "markdown", + "id": "7755d253", + "metadata": {}, + "source": [ + "Creating a table from a Pandas DataFrame:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "boring-wound", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.179243Z", + "start_time": "2021-09-21T21:40:04.024441Z" + } + }, + "outputs": [], + "source": [ + "df00 = sns.load_dataset(\"mpg\")\n", + "pandas_df = df00.describe().round(2).T[[\"mean\", \"std\", \"50%\"]]\n", + "\n", + "page[0] = es.DataFramePd(pandas_df)\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "alleged-botswana", + "metadata": {}, + "source": [ + "### Matplotlib Figures" + ] + }, + { + "cell_type": "markdown", + "id": "059b37c0", + "metadata": {}, + "source": [ + "Creating a plot from a Matplotlib figure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "floating-gambling", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.580046Z", + "start_time": "2021-09-21T21:40:04.187792Z" + }, + "scrolled": false + }, + "outputs": [], + "source": [ + "mpl_fig, ax = plt.subplots()\n", + "\n", + "colors = [\"C0\", \"C1\", \"C2\"]\n", + "df00.groupby(\"origin\")[\"horsepower\"].mean().plot.bar(color=colors, rot=0, ax=ax)\n", + "ax.set_title(\"Mean Horsepower by Origin\")\n", + "ax.set_ylabel(\"Horsepower\")\n", + "ax.set_xlabel(\"Origin\")\n", + "mpl_fig.tight_layout()\n", + "plt.close() # prevent auto-plotting in notebook\n", + "\n", + "page[0] = es.FigureMpl(mpl_fig)\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "olympic-portrait", + "metadata": {}, + "source": [ + "## Page Layout" + ] + }, + { + "cell_type": "markdown", + "id": "willing-seeker", + "metadata": {}, + "source": [ + "### Rows and Columns" + ] + }, + { + "cell_type": "markdown", + "id": "020f282c", + "metadata": {}, + "source": [ + "Pages are arranged in Sections, Rows, and Columns. If fine control over the layout is not required, the specific row\n", + "and / or column can be omitted and Esparto will infer the logical structure:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "sticky-aggregate", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.603041Z", + "start_time": "2021-09-21T21:40:04.587002Z" + } + }, + "outputs": [], + "source": [ + "my_page = es.Page(title=\"Page Title\")\n", + "my_page[0] = \"Page Content\"\n", + "my_page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "45490d85", + "metadata": {}, + "source": [ + "#### Add Content" + ] + }, + { + "cell_type": "markdown", + "id": "4bfaedd2", + "metadata": {}, + "source": [ + "Specifying the Section, Row, and Column:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "01fafd39", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.697115Z", + "start_time": "2021-09-21T21:40:04.610612Z" + } + }, + "outputs": [], + "source": [ + "page = es.Page(\"Page Title\")\n", + "page[\"Section One\"][\"Row One\"][\"Column One\"] = \"Some content\"\n", + "page[\"Section One\"][\"Row One\"][\"Column Two\"] = \"More content\"\n", + "page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "4cb5c48b", + "metadata": {}, + "source": [ + "Adding multiple Columns to the same Row using a tuple of dictionaries:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64870bc4", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.718851Z", + "start_time": "2021-09-21T21:40:04.704145Z" + } + }, + "outputs": [], + "source": [ + "page = es.Page(\"Page Title\")\n", + "page[\"Section One\"][\"Row One\"] = (\n", + " {\"Column One\": \"Some content\"}, \n", + " {\"Column Two\": \"More content\"}\n", + ")\n", + "page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "ae612283", + "metadata": {}, + "source": [ + "#### Update Content" + ] + }, + { + "cell_type": "markdown", + "id": "b21b2c6c", + "metadata": {}, + "source": [ + "Update content using indexing or object attributes:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9b51e9c4", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.740584Z", + "start_time": "2021-09-21T21:40:04.725959Z" + } + }, + "outputs": [], + "source": [ + "page[\"Section One\"][\"Row One\"][\"Column One\"] = image_path\n", + "page.section_one.row_one.column_two = image_path\n", + "page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "000f6b50", + "metadata": {}, + "source": [ + "#### Delete Content" + ] + }, + { + "cell_type": "markdown", + "id": "8451c647", + "metadata": {}, + "source": [ + "Delete content by index or attribute name:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4802abde", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.759446Z", + "start_time": "2021-09-21T21:40:04.747606Z" + } + }, + "outputs": [], + "source": [ + "del page.section_one.row_one[-1]\n", + "page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "b6d05194", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-20T20:00:46.759502Z", + "start_time": "2021-09-20T20:00:46.752587Z" + } + }, + "source": [ + "Alternatively, we can delete content by accessing its attribute:\n", + "```python\n", + "del page.section_one.row_one.new_title\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "dab93a0f", + "metadata": {}, + "source": [ + "#### Auto Layout" + ] + }, + { + "cell_type": "markdown", + "id": "20c741ea", + "metadata": {}, + "source": [ + "We can create a basic page with the methods covered so far:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b780b453", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.962342Z", + "start_time": "2021-09-21T21:40:04.767793Z" + } + }, + "outputs": [], + "source": [ + "my_page = es.Page(title=\"My Report\")\n", + "my_image = es.Image(\"my-image.png\", caption=image_credit)\n", + "my_page[\"Words and Images\"] = (\"markdown.md\", my_image)\n", + "\n", + "my_page" + ] + }, + { + "cell_type": "markdown", + "id": "5dcb98e3", + "metadata": {}, + "source": [ + "Esparto infers the full Page structure without explicit instructions. This code generates the same page as above:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "super-investor", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:04.988210Z", + "start_time": "2021-09-21T21:40:04.969294Z" + } + }, + "outputs": [], + "source": [ + "my_page_by_hand = es.Page(title=\"My Report\", children=[\n", + " es.Section(title=\"Words and Images\", children=[\n", + " es.Row(children=[\n", + " es.Column(children=[\"markdown.md\"]),\n", + " es.Column(children=[my_image])\n", + " ])\n", + " ])\n", + "])\n", + "my_page_by_hand.tree()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5c5dc42b", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.005435Z", + "start_time": "2021-09-21T21:40:04.996660Z" + } + }, + "outputs": [], + "source": [ + "assert my_page == my_page_by_hand\n", + "# True" + ] + }, + { + "cell_type": "markdown", + "id": "focused-northern", + "metadata": {}, + "source": [ + "### Content Cards" + ] + }, + { + "cell_type": "markdown", + "id": "1567abb9", + "metadata": {}, + "source": [ + "Cards are useful for grouping related content:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "burning-concrete", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.200715Z", + "start_time": "2021-09-21T21:40:05.012960Z" + } + }, + "outputs": [], + "source": [ + "data_sample = df00.iloc[:, :4].head(5)\n", + "data_types = df00.dtypes.rename(\"Data Type\").to_frame()\n", + "card_a = es.Card(title=\"Data Types\", children=[data_types])\n", + "card_b = es.Card(title=\"Summary Stats\", children=[mpl_fig, pandas_df])\n", + "card_a + card_b" + ] + }, + { + "cell_type": "markdown", + "id": "b1071950", + "metadata": {}, + "source": [ + "Adding Cards to a Section:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "arctic-terry", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.222809Z", + "start_time": "2021-09-21T21:40:05.208154Z" + } + }, + "outputs": [], + "source": [ + "section_two = es.Section(title=\"Data Analysis\")\n", + "section_two[0] = (card_a, card_b)\n", + "section_two.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "a41f553e", + "metadata": {}, + "source": [ + "### Card Sections" + ] + }, + { + "cell_type": "markdown", + "id": "d6adeb71", + "metadata": {}, + "source": [ + "Card Sections can be used when content should be wrapped in Cards by default:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "abc6fc93", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.253525Z", + "start_time": "2021-09-21T21:40:05.230967Z" + } + }, + "outputs": [], + "source": [ + "section_two = es.CardSection(title=\"Data Analysis\")\n", + "section_two[0] = (\n", + " {\"Data Types\": data_types},\n", + " {\"Summary Stats\": [mpl_fig, pandas_df]}\n", + ")\n", + "section_two.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "8ed77bbe", + "metadata": {}, + "source": [ + "### Using Spacers" + ] + }, + { + "cell_type": "markdown", + "id": "51b43d13", + "metadata": {}, + "source": [ + "Spacers create empty Columns in a Row:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "599cf1e2", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.360122Z", + "start_time": "2021-09-21T21:40:05.265236Z" + } + }, + "outputs": [], + "source": [ + "page = es.Page()\n", + "page[0] = ({\"Markdown\": markdown_text}, es.Spacer())\n", + "page[0]" + ] + }, + { + "cell_type": "markdown", + "id": "downtown-briefs", + "metadata": {}, + "source": [ + "### Page Breaks" + ] + }, + { + "cell_type": "markdown", + "id": "a86542d5", + "metadata": {}, + "source": [ + "Page Breaks indicate the end of a page when printing or converting to PDF:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "overhead-accreditation", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.383400Z", + "start_time": "2021-09-21T21:40:05.367053Z" + } + }, + "outputs": [], + "source": [ + "my_page[\"Data Analysis\"] = section_two\n", + "my_page.children.insert(1, es.PageBreak())\n", + "my_page.tree()" + ] + }, + { + "cell_type": "markdown", + "id": "1513a7ae", + "metadata": {}, + "source": [ + "## Esparto Options" + ] + }, + { + "cell_type": "markdown", + "id": "988db5ef", + "metadata": {}, + "source": [ + "The options available in `es.options` allow control over how dependencies are source and provisioned, \n", + "as well as the behaviour of certain content types." + ] + }, + { + "cell_type": "markdown", + "id": "a889cecb", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T17:10:36.545656Z", + "start_time": "2021-09-21T17:10:36.539808Z" + } + }, + "source": [ + "```python\n", + ">>> es.options?\n", + "```\n", + "```Options for configuring esparto behaviour and output.\n", + "\n", + "Config options will automatically be loaded if a yaml file is found at\n", + "either './esparto-config.yaml' or '~/esparto-data/esparto-config.yaml'.\n", + "\n", + "Attributes:\n", + " dependency_source (str):\n", + " How dependencies should be provisioned: 'cdn' or 'inline'.\n", + " bootstrap_cdn (str):\n", + " Link to Bootstrap CDN. Used if dependency source is 'cdn'.\n", + " Alternative links are available via esparto.bootstrap_cdn_themes.\n", + " bootstrap_css (str):\n", + " Path to Bootstrap CSS file. Used if dependency source is 'inline'.\n", + " esparto_css (str):\n", + " Path to additional CSS file with esparto specific styles.\n", + " jinja_template (str):\n", + " Path to Jinja HTML page template.\n", + "\n", + " matplotlib: Additional config options for Matplotlib.\n", + " plotly: Additional config options for Plotly.\n", + " bokeh: Additional config options for Bokeh.\n", + "```" + ] + }, + { + "cell_type": "markdown", + "id": "comprehensive-region", + "metadata": {}, + "source": [ + "## Saving your Work" + ] + }, + { + "cell_type": "markdown", + "id": "9726529e", + "metadata": {}, + "source": [ + "### As a Webpage" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "organizational-advice", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:05.912534Z", + "start_time": "2021-09-21T21:40:05.398749Z" + } + }, + "outputs": [], + "source": [ + "my_page.save_html(\"my-page.html\")" + ] + }, + { + "cell_type": "markdown", + "id": "de53c552", + "metadata": {}, + "source": [ + "### As a PDF" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "virtual-celebration", + "metadata": { + "ExecuteTime": { + "end_time": "2021-09-21T21:40:12.445181Z", + "start_time": "2021-09-21T21:40:05.921526Z" + } + }, + "outputs": [], + "source": [ + "my_page.save_pdf(\"my-page.pdf\")" + ] + } + ], + "metadata": { + "hide_input": false, + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5" + }, + "toc": { + "base_numbering": 1, + "nav_menu": {}, + "number_sections": true, + "sideBar": true, + "skip_h1_title": true, + "title_cell": "Table of Contents", + "title_sidebar": "Contents", + "toc_cell": false, + "toc_position": { + "height": "calc(100% - 180px)", + "left": "10px", + "top": "150px", + "width": "269px" + }, + "toc_section_display": true, + "toc_window_display": true + }, + "varInspector": { + "cols": { + "lenName": 16, + "lenType": 16, + "lenVar": 40 + }, + "kernels_config": { + "python": { + "delete_cmd_postfix": "", + "delete_cmd_prefix": "del ", + "library": "var_list.py", + "varRefreshCmd": "print(var_dic_list())" + }, + "r": { + "delete_cmd_postfix": ") ", + "delete_cmd_prefix": "rm(", + "library": "var_list.r", + "varRefreshCmd": "cat(var_dic_list()) " + } + }, + "types_to_exclude": [ + "module", + "function", + "builtin_function_or_method", + "instance", + "_Feature" + ], + "window_display": false + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/docs/examples/interactive-plots.html b/docs/examples/interactive-plots.html index 725ff89..1a0ecaf 100644 --- a/docs/examples/interactive-plots.html +++ b/docs/examples/interactive-plots.html @@ -28,7 +28,7 @@ page-break-after: auto; page-break-before: auto; } - .col-lg { + .es-column-body, .es-card { flex: 1 !important; } } @@ -36,11 +36,19 @@ /* Extra Small tables */ .table-xs td, th { - padding: .1rem !important; + padding: 0 !important; padding-left: .3rem !important; padding-right: .3rem !important; } +.table-xs th { + text-align: left; +} + +.table-xs td { + text-align: right; +} + /* Sticky footer styles */ html { @@ -81,47 +89,60 @@ -