Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ jobs:
with:
apt-packages: "texlive-latex-base texlive-latex-extra texlive-pictures texlive-fonts-recommended lmodern"
# stable-branch: main # deploy the latest release at /<repo>/ and devel at /<repo>/dev/
# a notebook's outputs are numbered in order: in tutorial 07, 1.html is the Plotly
# preview and 2.png the first compiled TikZ figure
smoke-paths: |
getting-started/installation/index.html
getting-started/quickstart/index.html
Expand All @@ -35,7 +37,7 @@ jobs:
tutorials/tutorial-01/index.html
tutorials/tutorial-07-tikz/index.html
tutorials/tutorial-17-xarray/index.html
tutorials/tutorial-07-tikz/1.png
tutorials/tutorial-07-tikz/2.png
tutorials/tutorial-01/tutorial_01.ipynb
api/maxplotlib/index.html
api/maxplotlib/canvas/index.html
7 changes: 7 additions & 0 deletions src/maxplotlib/canvas/canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -2891,6 +2891,13 @@ def plot_plotly(
"""

self._validate_import_backend("plotly", allow_unsupported=allow_unsupported)
if any(
isinstance(subplot, TikzFigure) for _, _, subplot in self.iter_subplots()
):
raise NotImplementedError(
"tikzfigure subplots cannot be rendered with the plotly backend; "
"use the matplotlib or tikzfigure backend"
)
resolved_usetex = self._usetex if usetex is None else usetex

for subplot in self._subplot_dict.values():
Expand Down
13 changes: 13 additions & 0 deletions src/maxplotlib/tests/test_canvas.py
Original file line number Diff line number Diff line change
Expand Up @@ -1378,3 +1378,16 @@ def test_plotly_rendering_keeps_the_global_style():
style = dict(plt.rcParams)
canvas.plot_plotly()
assert dict(plt.rcParams) == style


def test_plotly_refuses_tikzfigure_subplots():
import pytest

pytest.importorskip("plotly")
from maxplotlib import Canvas

canvas = Canvas()
diagram = canvas.add_tikzfigure()
diagram.add_node(0, 0, label="A")
with pytest.raises(NotImplementedError, match="plotly backend"):
canvas.render(backend="plotly")
49 changes: 49 additions & 0 deletions tutorials/tutorial_08_plotly.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,55 @@
"cell_type": "markdown",
"id": "20",
"metadata": {},
"source": [
"### Smaller files and embedding\n",
"\n",
"A standalone file bundles plotly.js, several megabytes. `include_plotlyjs` chooses\n",
"where it comes from: `True` (the default of `savefig`) bundles it, `\"cdn\"` loads it\n",
"from a CDN when the page opens, which keeps the file a few kilobytes but needs a\n",
"network connection, and `False` leaves it out for a page that loads plotly.js itself."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "21",
"metadata": {},
"outputs": [],
"source": [
"import os\n",
"\n",
"canvas.savefig(\"figure_bundled.html\", backend=\"plotly\")\n",
"canvas.savefig(\"figure_cdn.html\", backend=\"plotly\", include_plotlyjs=\"cdn\")\n",
"for name in (\"figure_bundled.html\", \"figure_cdn.html\"):\n",
" print(f\"{name}: {os.path.getsize(name) / 1024:.0f} kB\")"
]
},
{
"cell_type": "markdown",
"id": "22",
"metadata": {},
"source": [
"`canvas.to_html()` returns the HTML as a string instead, with plotly.js from the CDN\n",
"by default. `full_html=False` gives just the `<div>` and `<script>`, to put into a\n",
"page of your own:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "23",
"metadata": {},
"outputs": [],
"source": [
"fragment = canvas.to_html(include_plotlyjs=\"cdn\", full_html=False)\n",
"print(fragment[:200])"
]
},
{
"cell_type": "markdown",
"id": "24",
"metadata": {},
"source": [
"## Summary\n",
"\n",
Expand Down
220 changes: 220 additions & 0 deletions tutorials/tutorial_18_matplotlib_import.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# Importing Matplotlib figures\n",
"\n",
"`Canvas.from_matplotlib` turns a finished Matplotlib figure into a canvas, so a figure\n",
"from another library or an old script can be drawn again with the TikZ, Plotly or\n",
"plotext backend. The [Matplotlib import](/maxplotlib/guides/matplotlib-import/) guide lists\n",
"everything that is imported; this tutorial shows the workflow."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from maxplotlib import Canvas"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## A figure made with Matplotlib\n",
"\n",
"Two panels: lines, colored markers and a legend on the left, an image with a color\n",
"bar and an annotation on the right."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"x = np.linspace(0, 2 * np.pi, 60)\n",
"fig, (left, right) = plt.subplots(1, 2, figsize=(9, 3.4))\n",
"left.plot(x, np.sin(x), label=\"sin\")\n",
"left.scatter(x[::6], np.cos(x[::6]), c=x[::6], label=\"cos samples\")\n",
"left.set(title=\"Lines and markers\", xlabel=\"x\")\n",
"left.legend()\n",
"\n",
"field = np.outer(np.sin(x), np.cos(x))\n",
"image = right.imshow(field, origin=\"lower\", cmap=\"RdBu_r\")\n",
"fig.colorbar(image, ax=right, label=\"field\")\n",
"right.annotate(\"peak\", (15, 0), xytext=(35, 20), arrowprops={\"arrowstyle\": \"->\"})\n",
"right.set_title(\"Image\")\n",
"plt.close(fig) # shown below through the canvas"
]
},
{
"cell_type": "markdown",
"id": "4",
"metadata": {},
"source": [
"## Importing it\n",
"\n",
"The canvas keeps the layout, the panels and every artist. With the Matplotlib\n",
"backend it looks like the original."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"canvas = Canvas.from_matplotlib(fig)\n",
"fig_imported, axes = canvas.show()"
]
},
{
"cell_type": "markdown",
"id": "6",
"metadata": {},
"source": [
"## What was imported\n",
"\n",
"`canvas.import_report` lists one diagnostic per artist: how it was imported\n",
"(`fallback`), and which backends can draw it (`backends`)."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "7",
"metadata": {},
"outputs": [],
"source": [
"for item in canvas.import_report.diagnostics:\n",
" print(\n",
" f\"{item.artist_type:15} {item.severity:5} fallback={item.fallback}: {item.message}\"\n",
" )"
]
},
{
"cell_type": "markdown",
"id": "8",
"metadata": {},
"source": [
"## Other backends\n",
"\n",
"Lines, markers and images become plot entries that every backend draws. Some\n",
"Matplotlib features are kept as native Matplotlib geometry instead, here the\n",
"color bar, and a backend that cannot draw them refuses with a clear error:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "9",
"metadata": {},
"outputs": [],
"source": [
"try:\n",
" canvas.show(backend=\"plotly\")\n",
"except NotImplementedError as error:\n",
" print(error)"
]
},
{
"cell_type": "markdown",
"id": "10",
"metadata": {},
"source": [
"`fallback=\"raster\"` flattens the whole figure into one image, which every\n",
"backend shows; `fallback=\"skip\"` leaves the unsupported artists out with a warning."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"flat = Canvas.from_matplotlib(fig, fallback=\"raster\")\n",
"flat.show(backend=\"plotly\")"
]
},
{
"cell_type": "markdown",
"id": "12",
"metadata": {},
"source": [
"## Importing one panel\n",
"\n",
"An `Axes` (or an array of them) imports just those panels. The left panel has only\n",
"portable artists, so it converts to TikZ/pgfplots code for a paper:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "13",
"metadata": {},
"outputs": [],
"source": [
"panel = Canvas.from_matplotlib(left, width=\"8cm\", ratio=0.6)\n",
"tikz = panel.render(backend=\"tikzfigure\")\n",
"print(str(tikz)[:600])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "14",
"metadata": {},
"outputs": [],
"source": [
"panel.show(backend=\"tikzfigure\")"
]
},
{
"cell_type": "markdown",
"id": "15",
"metadata": {},
"source": [
"## Strict imports and files\n",
"\n",
"`strict=True` raises instead of warning when something cannot be imported, for\n",
"scripts that must not lose anything silently. `from_matplotlib` also reads a\n",
"pickled figure from a path or bytes, but only with `trusted=True`: unpickling can\n",
"run arbitrary code, so read only files you made yourself."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"metadata": {},
"outputs": [],
"source": [
"strict = Canvas.from_matplotlib(left, strict=True)\n",
"print(len(strict.import_report.diagnostics), \"artists imported\")"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading