diff --git a/.gitignore b/.gitignore
index fe33072..c939b39 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,8 @@
# Scratch files for testing
scratch.ipynb
scratch.py
-esparto-page-*.html
-notebooks/*.jpg
+esparto-doc.html
+docs/examples/*.html
# IDE files
.vscode
diff --git a/.travis.yml b/.travis.yml
index 0811cdc..b994d60 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,21 +1,31 @@
+sudo: false
language: python
os: linux
dist: xenial
python:
+ - 3.9
- 3.8
- 3.7
- 3.6
+env:
+ - INSTALL_DEPS=""
+ - INSTALL_DEPS="--no-dev"
+
before_install:
- - pip install poetry
+ - curl -fsS -o get-poetry.py https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py
+ - python get-poetry.py -y
+ - export PATH="$PATH:$HOME/.poetry/bin"
+ - poetry config virtualenvs.in-project true
install:
- - poetry install
+ - poetry install $INSTALL_DEPS -E test
script:
- black --check .
- flake8
- - coverage run --source esparto -m pytest
+ - mypy esparto tests
+ - coverage run --append --source esparto -m pytest
after_success:
- bash <(curl -s https://codecov.io/bash)
diff --git a/Makefile b/Makefile
index 6569769..7ead546 100644
--- a/Makefile
+++ b/Makefile
@@ -61,9 +61,11 @@ mypy: ## check type hints
test: ## run tests quickly with the default Python
pytest
+ python -m tests.check_package_version
test-all: ## run tests on every Python version with tox
- tox
+ tox --skip-missing-interpreters
+ python -m tests.check_package_version
coverage: ## check code coverage quickly with the default Python
coverage run --source esparto -m pytest
diff --git a/docs/01-getting-started/quickstart.md b/docs/01-getting-started/quickstart.md
index a282365..be8731d 100644
--- a/docs/01-getting-started/quickstart.md
+++ b/docs/01-getting-started/quickstart.md
@@ -57,6 +57,6 @@ Esparto determines that the string points to a valid image and loads the file:
-Please check the [examples page](../02-user-guide/examples.md) for a more in-depth guide.
+Please see the [examples page](../02-user-guide/examples.md) for more.
diff --git a/docs/02-user-guide/examples.md b/docs/02-user-guide/examples.md
index 7cd41b0..c07e4f6 100644
--- a/docs/02-user-guide/examples.md
+++ b/docs/02-user-guide/examples.md
@@ -1,13 +1,13 @@
# Examples
-### Data Analysis Report
+### Data Analysis
-[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/domvwt/esparto/blob/main/docs/examples/iris.ipynb)
-[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/domvwt/esparto/main?filepath=docs%2Fexamples%2Firis.ipynb)
+[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/domvwt/esparto/blob/main/docs/examples/iris-report.ipynb)
+[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/domvwt/esparto/main?filepath=docs%2Fexamples%2Firis-report.ipynb)
The iris dataset is one of the most well known datasets in statistics and data science. This example notebook shows how we can put together a simple data analysis report in esparto.
-This example covers
+This example covers:
* Text content with markdown formatting
* Including images from files
@@ -15,3 +15,21 @@ This example covers
* Adding plots from Matplotlib and Seaborn
+
+### Interactive Plotting
+
+[![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)
+
+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 example will show basic examples from each library:
+
+* Interactive plotting with Bokeh and Plotly
+* Adding interactive content to the page
+
+
diff --git a/docs/03-api-reference/adaptors.md b/docs/03-api-reference/adaptors.md
index 4bcbd30..ccb7a33 100644
--- a/docs/03-api-reference/adaptors.md
+++ b/docs/03-api-reference/adaptors.md
@@ -2,13 +2,20 @@
!!! info
The ```content_adaptor``` is called internally when an explicit Content class is not provided.
+ Input objects are matched to a suitable Content class through [_single dispatch_](https://docs.python.org/3/library/functools.html#functools.singledispatch).
+
``` python
import esparto as es
- # Create some new Markdown text
- markdown = es.Markdown("Example _markdown_ text.")
-
+ # Text automatically converted to Markdown content.
+ section = es.Section()
+ section += "Example _markdown_ text."
+ print(section)
+ ```
```
+ {'Section': [{'Row': [{'Column': ['Markdown']}]}]}
+ ```
+
## ::: esparto._adaptors
diff --git a/docs/03-api-reference/content.md b/docs/03-api-reference/content.md
index e0c9e3b..e18475b 100644
--- a/docs/03-api-reference/content.md
+++ b/docs/03-api-reference/content.md
@@ -22,4 +22,8 @@
## ::: esparto._content.FigureMpl
+## ::: esparto._content.FigureBokeh
+
+## ::: esparto._content.FigurePlotly
+
diff --git a/docs/examples/interactive-plots.ipynb b/docs/examples/interactive-plots.ipynb
new file mode 100644
index 0000000..6307988
--- /dev/null
+++ b/docs/examples/interactive-plots.ipynb
@@ -0,0 +1,12390 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "Oy98EV4EJcOy"
+ },
+ "source": [
+ "# Interactive Plots with Bokeh and Plotly\n",
+ "This notebook demonstrates how we can incorporate interactive plots from Bokeh and Plotly into a page.\n",
+ "\n",
+ "\n",
+ "We will look at:\n",
+ "* Interactive plotting with Bokeh and Plotly\n",
+ "* Adding interactive content to the page"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "metadata": {
+ "colab": {
+ "base_uri": "https://localhost:8080/"
+ },
+ "id": "C30QlbKwJcPE",
+ "outputId": "162f1970-6e9c-42a9-f5ae-de6341e93dc8"
+ },
+ "outputs": [],
+ "source": [
+ "# Environment setup\n",
+ "import os\n",
+ "!pip install -Uqq esparto plotly bokeh pandas-bokeh\n",
+ "if os.environ.get(\"BINDER_SERVICE_HOST\"):\n",
+ " !pip install -Uqq pandas"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "metadata": {
+ "id": "0Pzi4t5EsUlq"
+ },
+ "outputs": [],
+ "source": [
+ "import esparto as es\n",
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import pandas_bokeh\n",
+ "import bokeh as bk\n",
+ "import plotly"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "
\n",
+ " \n",
+ " Loading BokehJS ...\n",
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "\n",
+ "(function(root) {\n",
+ " function now() {\n",
+ " return new Date();\n",
+ " }\n",
+ "\n",
+ " var force = true;\n",
+ "\n",
+ " if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n",
+ " root._bokeh_onload_callbacks = [];\n",
+ " root._bokeh_is_loading = undefined;\n",
+ " }\n",
+ "\n",
+ " var JS_MIME_TYPE = 'application/javascript';\n",
+ " var HTML_MIME_TYPE = 'text/html';\n",
+ " var EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n",
+ " var CLASS_NAME = 'output_bokeh rendered_html';\n",
+ "\n",
+ " /**\n",
+ " * Render data to the DOM node\n",
+ " */\n",
+ " function render(props, node) {\n",
+ " var script = document.createElement(\"script\");\n",
+ " node.appendChild(script);\n",
+ " }\n",
+ "\n",
+ " /**\n",
+ " * Handle when an output is cleared or removed\n",
+ " */\n",
+ " function handleClearOutput(event, handle) {\n",
+ " var cell = handle.cell;\n",
+ "\n",
+ " var id = cell.output_area._bokeh_element_id;\n",
+ " var server_id = cell.output_area._bokeh_server_id;\n",
+ " // Clean up Bokeh references\n",
+ " if (id != null && id in Bokeh.index) {\n",
+ " Bokeh.index[id].model.document.clear();\n",
+ " delete Bokeh.index[id];\n",
+ " }\n",
+ "\n",
+ " if (server_id !== undefined) {\n",
+ " // Clean up Bokeh references\n",
+ " var cmd = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n",
+ " cell.notebook.kernel.execute(cmd, {\n",
+ " iopub: {\n",
+ " output: function(msg) {\n",
+ " var id = msg.content.text.trim();\n",
+ " if (id in Bokeh.index) {\n",
+ " Bokeh.index[id].model.document.clear();\n",
+ " delete Bokeh.index[id];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " // Destroy server and session\n",
+ " var cmd = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n",
+ " cell.notebook.kernel.execute(cmd);\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " /**\n",
+ " * Handle when a new output is added\n",
+ " */\n",
+ " function handleAddOutput(event, handle) {\n",
+ " var output_area = handle.output_area;\n",
+ " var output = handle.output;\n",
+ "\n",
+ " // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n",
+ " if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n",
+ " return\n",
+ " }\n",
+ "\n",
+ " var toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n",
+ "\n",
+ " if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n",
+ " toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n",
+ " // store reference to embed id on output_area\n",
+ " output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n",
+ " }\n",
+ " if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n",
+ " var bk_div = document.createElement(\"div\");\n",
+ " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n",
+ " var script_attrs = bk_div.children[0].attributes;\n",
+ " for (var i = 0; i < script_attrs.length; i++) {\n",
+ " toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n",
+ " toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n",
+ " }\n",
+ " // store reference to server id on output_area\n",
+ " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " function register_renderer(events, OutputArea) {\n",
+ "\n",
+ " function append_mime(data, metadata, element) {\n",
+ " // create a DOM node to render to\n",
+ " var toinsert = this.create_output_subarea(\n",
+ " metadata,\n",
+ " CLASS_NAME,\n",
+ " EXEC_MIME_TYPE\n",
+ " );\n",
+ " this.keyboard_manager.register_events(toinsert);\n",
+ " // Render to node\n",
+ " var props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n",
+ " render(props, toinsert[toinsert.length - 1]);\n",
+ " element.append(toinsert);\n",
+ " return toinsert\n",
+ " }\n",
+ "\n",
+ " /* Handle when an output is cleared or removed */\n",
+ " events.on('clear_output.CodeCell', handleClearOutput);\n",
+ " events.on('delete.Cell', handleClearOutput);\n",
+ "\n",
+ " /* Handle when a new output is added */\n",
+ " events.on('output_added.OutputArea', handleAddOutput);\n",
+ "\n",
+ " /**\n",
+ " * Register the mime type and append_mime function with output_area\n",
+ " */\n",
+ " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n",
+ " /* Is output safe? */\n",
+ " safe: true,\n",
+ " /* Index of renderer in `output_area.display_order` */\n",
+ " index: 0\n",
+ " });\n",
+ " }\n",
+ "\n",
+ " // register the mime type if in Jupyter Notebook environment and previously unregistered\n",
+ " if (root.Jupyter !== undefined) {\n",
+ " var events = require('base/js/events');\n",
+ " var OutputArea = require('notebook/js/outputarea').OutputArea;\n",
+ "\n",
+ " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n",
+ " register_renderer(events, OutputArea);\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " \n",
+ " if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n",
+ " root._bokeh_timeout = Date.now() + 5000;\n",
+ " root._bokeh_failed_load = false;\n",
+ " }\n",
+ "\n",
+ " var NB_LOAD_WARNING = {'data': {'text/html':\n",
+ " \"
\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n",
+ " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"
re-rerun `output_notebook()` to attempt to load from CDN again, or
\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"
\\n\"+\n \"
\\n\"+\n \"
re-rerun `output_notebook()` to attempt to load from CDN again, or
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plotly_scatter = df_mpg.plot.scatter(\n",
+ " title=\"Cars\",\n",
+ " x=\"mpg\", y=\"acceleration\", \n",
+ " color=\"model_year\",\n",
+ " color_continuous_scale=\"burg_r\",\n",
+ " facet_col=\"origin\",\n",
+ " backend=\"plotly\", \n",
+ " template=\"plotly_white\",\n",
+ ")\n",
+ "plotly_scatter.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "metadata": {
+ "id": "-_DFIxBq6wa5"
+ },
+ "outputs": [],
+ "source": [
+ "my_page = es.Page(title=\"Interactive Plots\")\n",
+ "\n",
+ "bokeh_section = es.Section(title=\"Bokeh\")\n",
+ "bokeh_section += \"\"\"\n",
+ "\n",
+ "The [pandas-bokeh](https://github.com/PatrikHlobil/Pandas-Bokeh) library offers \n",
+ "convenient functions for producing interactive Bokeh plots with few lines of code.\n",
+ "\n",
+ "Bokeh figures will preserve their default aspect ratio by default - though this behaviour\n",
+ "can be configured through the Bokeh figure object in some cases. Composite objects, such\n",
+ "as the line chart with range slider, may need to be created using the Bokeh core API in\n",
+ "order to configure them correctly.\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "bokeh_lines.sizing_mode = \"stretch_width\" # Has no effect as this is a composite object\n",
+ "bokeh_scatter.sizing_mode = \"stretch_width\" # This plot will be responsive to screen width\n",
+ "\n",
+ "bokeh_section += (\n",
+ " es.Row(es.Column(bokeh_lines, title=\"Line Plot with Range Slider\")), \n",
+ " es.Row(es.Column(bokeh_scatter, title=\"Scatter Plot\"))\n",
+ ")\n",
+ "\n",
+ "\n",
+ "plotly_section = es.Section(title=\"Plotly\")\n",
+ "plotly_section += \"\"\"\n",
+ "\n",
+ "With the [Plotly backend for Pandas](https://plotly.com/python/pandas-backend/) \n",
+ "we can access the Plotly Express API directly from the '.plot()' method of any DataFrame or Series.\n",
+ "\n",
+ "Plotly figures will expand to fill their container space by default. \n",
+ "All [esparto](https://domvwt.github.io/esparto/) figure classes can be manually adjusted \n",
+ "through their width and height attributes.\n",
+ "\n",
+ "\"\"\"\n",
+ "\n",
+ "plotly_section += (\n",
+ " es.Row(es.Column(plotly_lines, title=\"Line Plot\")),\n",
+ " es.Row(es.Column(plotly_scatter, title=\"Scatter Plot with Facet Columns\"))\n",
+ ")\n",
+ "\n",
+ "my_page += bokeh_section, plotly_section"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Please note that in-notebook page rendering can be temperamental when using certain components due to \n",
+ "timing issues while waiting for CDN content. If the page does not render, try rerunning the cell a few \n",
+ "times and it will eventually appear. You should have no issues when opening the HTML document."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 14,
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "\n",
+ " \n",
+ " \n",
+ "\n",
+ "
\n",
+ "\n",
+ "
Interactive Plots
\n",
+ "\n",
+ "
\n",
+ "
Bokeh
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
The pandas-bokeh library offers \n",
+ "convenient functions for producing interactive Bokeh plots with few lines of code.
\n",
+ "
Bokeh figures will preserve their default aspect ratio by default - though this behaviour\n",
+ "can be configured through the Bokeh figure object in some cases. Composite objects, such\n",
+ "as the line chart with range slider, may need to be created using the Bokeh core API in\n",
+ "order to configure them correctly.
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
Line Plot with Range Slider
\n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
Scatter Plot
\n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
Plotly
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
With the Plotly backend for Pandas \n",
+ "we can access the Plotly Express API directly from the '.plot()' method of any DataFrame or Series.
\n",
+ "
Plotly figures will expand to fill their container space by default. \n",
+ "All esparto figure classes can be manually adjusted \n",
+ "through their width and height attributes.
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
Line Plot
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "
Scatter Plot with Facet Columns
\n",
+ "\n",
+ "
\n",
+ "
\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "\n",
+ "
\n",
+ "\n",
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " \n",
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {
+ "isolated": true
+ },
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "\n"
+ ]
+ },
+ {
+ "data": {
+ "application/javascript": [
+ "$('.output_scroll').removeClass('output_scroll')"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/plain": [
+ ": Interactive Plots"
+ ]
+ },
+ "execution_count": 14,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "my_page"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {
+ "id": "jeDYcQ2iJcPp"
+ },
+ "source": [
+ "We can now save our page to an HTML file and share it."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "metadata": {
+ "id": "05oH3hyt7Bvh"
+ },
+ "outputs": [],
+ "source": [
+ "page_name = \"interactive-plots.html\"\n",
+ "my_page.save(page_name)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "Check your current working directory for the finished report!"
+ ]
+ }
+ ],
+ "metadata": {
+ "colab": {
+ "collapsed_sections": [],
+ "name": "esparto-iris-example-colab.ipynb",
+ "provenance": []
+ },
+ "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"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 1
+}
diff --git a/docs/examples/iris.ipynb b/docs/examples/iris-report.ipynb
similarity index 100%
rename from docs/examples/iris.ipynb
rename to docs/examples/iris-report.ipynb
diff --git a/docs/index.md b/docs/index.md
index 9b58a27..1cb542c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -34,5 +34,7 @@ The following content types are currently supported
* Images
* Matplotlib figures
* Pandas DataFrames
+* Bokeh objects
+* Plotly figures
diff --git a/esparto/__init__.py b/esparto/__init__.py
index b81eff7..4d58287 100644
--- a/esparto/__init__.py
+++ b/esparto/__init__.py
@@ -6,19 +6,28 @@
__author__ = """Dominic Thorn"""
__email__ = "dominic.thorn@gmail.com"
-__version__ = "0.1.0"
+__version__ = "0.2.0"
-_OPTIONAL_DEPENDENCIES: list = [
+_OPTIONAL_DEPENDENCIES: set = {
"bs4",
"IPython",
"matplotlib",
"pandas",
-]
+ "bokeh",
+ "plotly",
+}
_INSTALLED_MODULES: Set[str] = {
x.name for x in [_find_spec(dep) for dep in _OPTIONAL_DEPENDENCIES] if x
}
-from esparto._content import DataFramePd, FigureMpl, Image, Markdown
+from esparto._content import (
+ DataFramePd,
+ FigureBokeh,
+ FigureMpl,
+ FigurePlotly,
+ Image,
+ Markdown,
+)
from esparto._layout import Column, Page, Row, Section
diff --git a/esparto/_adaptors.py b/esparto/_adaptors.py
index d8ffee3..c75168f 100644
--- a/esparto/_adaptors.py
+++ b/esparto/_adaptors.py
@@ -2,7 +2,15 @@
from mimetypes import guess_type
from esparto import _INSTALLED_MODULES
-from esparto._content import Content, DataFramePd, FigureMpl, Image, Markdown
+from esparto._content import (
+ Content,
+ DataFramePd,
+ FigureBokeh,
+ FigureMpl,
+ FigurePlotly,
+ Image,
+ Markdown,
+)
@singledispatch
@@ -18,14 +26,14 @@ def content_adaptor(content: Content) -> Content:
"""
if not issubclass(type(content), Content):
- raise TypeError("Unsupported content type.")
+ raise TypeError(f"Unsupported content type: {type(content)}")
return content
@content_adaptor.register(str)
def content_adaptor_core(content: str) -> Content:
- """Called through dynamic dispatch."""
+ """Convert markdown or image to Markdown or Image content."""
guess = guess_type(content)
if guess and "image" in str(guess[0]):
return Image(content)
@@ -39,7 +47,7 @@ def content_adaptor_core(content: str) -> Content:
@content_adaptor.register(DataFrame)
def content_adaptor_df(content: DataFrame) -> DataFramePd:
- """Called through dynamic dispatch."""
+ """Convert Pandas DataFrame to DataFramePD content."""
return DataFramePd(content)
@@ -49,5 +57,25 @@ def content_adaptor_df(content: DataFrame) -> DataFramePd:
@content_adaptor.register(Figure)
def content_adaptor_fig(content: Figure) -> FigureMpl:
- """Called through dynamic dispatch."""
+ """Convert Matplotlib Figure to FigureMpl content."""
return FigureMpl(content)
+
+
+# Function only available if Bokeh is installed.
+if "bokeh" in _INSTALLED_MODULES:
+ from bokeh.layouts import LayoutDOM as BokehObject # type: ignore
+
+ @content_adaptor.register(BokehObject)
+ def content_adaptor_bokeh_layout(content: BokehObject) -> FigureBokeh:
+ """Convert Bokeh Layout to FigureBokeh content."""
+ return FigureBokeh(content)
+
+
+# Function only available if Plotly is installed.
+if "plotly" in _INSTALLED_MODULES:
+ from plotly.graph_objs._figure import Figure as PlotlyFigure # type: ignore
+
+ @content_adaptor.register(PlotlyFigure)
+ def content_adaptor_plotly(content: PlotlyFigure) -> FigurePlotly:
+ """Convert Plotly Figure to FigurePlotly content."""
+ return FigurePlotly(content)
diff --git a/esparto/_content.py b/esparto/_content.py
index b6f10c2..ad259d1 100644
--- a/esparto/_content.py
+++ b/esparto/_content.py
@@ -3,7 +3,7 @@
import base64
from abc import ABC, abstractmethod
from io import BytesIO
-from typing import Any, Union
+from typing import Any, Set, Union
import markdown as md
import PIL.Image as Img # type: ignore
@@ -12,11 +12,19 @@
from esparto import _INSTALLED_MODULES
from esparto._publish import nb_display
-if "pandas" in _INSTALLED_MODULES: # pragma: no cover
+if "pandas" in _INSTALLED_MODULES:
from pandas import DataFrame # type: ignore
-if "matplotlib" in _INSTALLED_MODULES: # pragma: no cover
- from matplotlib.figure import Figure # type: ignore
+if "matplotlib" in _INSTALLED_MODULES:
+ from matplotlib.figure import Figure as MplFigure # type: ignore
+
+if "bokeh" in _INSTALLED_MODULES:
+ from bokeh.embed import components # type: ignore
+ from bokeh.models.layouts import LayoutDOM as BokehObject # type: ignore
+
+if "plotly" in _INSTALLED_MODULES:
+ from plotly.graph_objs._figure import Figure as PlotlyFigure # type: ignore
+ from plotly.io import to_html as plotly_to_html # type: ignore
def _image_to_base64(image: PILImage) -> str:
@@ -62,6 +70,20 @@ def display(self) -> None:
"""Display rendered content in a Jupyter Notebook cell."""
nb_display(self)
+ @property
+ def _dependencies(self) -> Set[str]:
+ raise NotImplementedError
+
+ @_dependencies.getter
+ def _dependencies(self) -> Set[str]:
+ if hasattr(self, "_deps"):
+ return self._deps
+ return set()
+
+ @_dependencies.setter
+ def _dependencies(self, deps) -> None:
+ self._deps = deps
+
def __add__(self, other):
from esparto._layout import Row # Deferred for evade circular import
@@ -82,7 +104,10 @@ def __str__(self):
def __eq__(self, other):
if isinstance(other, self.__class__):
- return all(x == y for x, y in zip(self.content, other.content))
+ if hasattr(self.content, "__iter__") and hasattr(other.content, "__iter__"):
+ return all(x == y for x, y in zip(self.content, other.content))
+ else:
+ return self.content == other.content
return False
@@ -119,9 +144,9 @@ def __init__(self, text):
raise TypeError(r"text must be str")
self.content = str(text)
+ self._dependencies = set()
def to_html(self) -> str:
- """ """
html = md.markdown(self.content)
html = f"{html}\n"
html = f"
\n{html}\n
"
@@ -202,7 +227,6 @@ def rescale(self, scale) -> "Image":
return self
def to_html(self) -> str:
- """ """
if isinstance(self.content, PILImage):
image = self.content
else:
@@ -271,7 +295,6 @@ def __init__(
self.col_space = col_space
def to_html(self) -> str:
- """ """
classes = "table table-sm table-striped table-hover table-bordered"
html = self.content.to_html(
index=self.index, border=0, col_space=self.col_space, classes=classes
@@ -286,20 +309,199 @@ class FigureMpl(Image):
figure (plt.Figure): A Matplotlib figure.
caption (str): Image caption (default = None)
alt_text (str): Alternative text. (default = None)
- scale (float): Value by which to scale image, must be > 0 and <= 1. (default = 1)
"""
def __init__(
self,
- figure: "Figure",
+ figure: "MplFigure",
caption: str = "",
alt_text: str = "Image",
):
- if not isinstance(figure, Figure):
+ if not isinstance(figure, MplFigure):
raise TypeError(r"figure must be a Matplotlib Figure")
buffer = BytesIO()
figure.savefig(buffer, format="png")
super().__init__(buffer, scale=1, caption=caption, alt_text=alt_text)
+
+
+class FigureBokeh(Content):
+ """Bokeh object to be rendered as an interactive plot.
+
+ Args:
+ figure (bokeh.layouts.LayoutDOM): A Bokeh object.
+ width (int): Width in pixels. (default = 'auto')
+ height (int): Height in pixels. (default = 'auto')
+
+ """
+
+ @property
+ def content(self) -> "BokehObject":
+ """ """
+ raise NotImplementedError
+
+ @content.getter
+ def content(self) -> "BokehObject":
+ """ """
+ return self._content
+
+ @content.setter
+ def content(self, content) -> None:
+ """ """
+ self._content = content
+
+ @property
+ def width(self) -> Union[int, str, None]:
+ """ """
+ raise NotImplementedError
+
+ @width.getter
+ def width(self) -> str:
+ """ """
+ if isinstance(self._width, str) and self._width == "auto":
+ return self._width
+
+ return f"{self._width}px"
+
+ @width.setter
+ def width(self, width) -> None:
+ """ """
+ self._width = width
+
+ @property
+ def height(self) -> Union[int, str, None]:
+ """ """
+ raise NotImplementedError
+
+ @height.getter
+ def height(self) -> str:
+ """ """
+ if isinstance(self._height, str) and self._height == "auto":
+ return self._height
+
+ return f"{self._height}px"
+
+ @height.setter
+ def height(self, height) -> None:
+ """ """
+ self._height = height
+
+ def __init__(
+ self,
+ figure: "BokehObject",
+ width: int = None,
+ height: int = None,
+ ):
+
+ self._dependencies = {"bokeh"}
+ self.content = figure
+
+ if not issubclass(type(figure), BokehObject):
+ raise TypeError(r"figure must be a Bokeh object")
+
+ self.width = width or "auto"
+ self.height = height or "auto"
+
+ # Required as deep copy is not defined for Bokeh figures
+ # Also need to catch some erroneous args that get passed to the function
+ def __deepcopy__(self, *args, **kwargs):
+ cls = self.__class__
+ return cls(self.content)
+
+ def to_html(self) -> str:
+ html, js = components(self.content)
+
+ # Remove outer
tag so we can give our own attributes
+ html = _remove_outer_div(html)
+
+ return f"