mplbed 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mplbed-0.1.0/PKG-INFO +74 -0
- mplbed-0.1.0/README.md +63 -0
- mplbed-0.1.0/pyproject.toml +81 -0
- mplbed-0.1.0/src/mplbed/__init__.py +15 -0
- mplbed-0.1.0/src/mplbed/_doc_helpers.py +52 -0
- mplbed-0.1.0/src/mplbed/asgi.py +121 -0
- mplbed-0.1.0/src/mplbed/consts.py +2 -0
- mplbed-0.1.0/src/mplbed/html/__init__.py +0 -0
- mplbed-0.1.0/src/mplbed/html/_impl.py +149 -0
- mplbed-0.1.0/src/mplbed/html/raw.py +15 -0
- mplbed-0.1.0/src/mplbed/html/safe.py +22 -0
- mplbed-0.1.0/src/mplbed/integration/_common.py +291 -0
- mplbed-0.1.0/src/mplbed/integration/quart.py +213 -0
- mplbed-0.1.0/src/mplbed/integration/starlette.py +191 -0
- mplbed-0.1.0/src/mplbed/server/__init__.py +3 -0
- mplbed-0.1.0/src/mplbed/server/_impl.py +170 -0
- mplbed-0.1.0/src/mplbed/server/_utils.py +33 -0
- mplbed-0.1.0/src/mplbed/webaggext/__init__.py +16 -0
- mplbed-0.1.0/src/mplbed/webaggext/_impl.py +153 -0
- mplbed-0.1.0/src/mplbed/webaggext/webaggext.js +160 -0
mplbed-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: mplbed
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Integrate Matplotlib's WebAgg backend with Starlette for real-time plotting in web applications.
|
|
5
|
+
Requires-Dist: frozendict>=2.4.7
|
|
6
|
+
Requires-Dist: matplotlib>=3.10.9
|
|
7
|
+
Requires-Dist: starlette>=1.0.0
|
|
8
|
+
Requires-Dist: tornado>=6.5.5
|
|
9
|
+
Requires-Python: >=3.14
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# mplbed
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
|[]mpl
|
|
16
|
+
|======|
|
|
17
|
+
| bed |
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
`mplbed` is a library of support code for embedding interactive, server-side matplotlib figures in web applications.
|
|
21
|
+
|
|
22
|
+
Supporting, in principle any ASGI-compatible Python application, it provides convenient integration for a number of frameworks ones out of the box.
|
|
23
|
+
It aims to have "usually-works" defaults useful for quick demos, while providing a lot of flexibility and options as well as utilities to deal with advanced issues such as style isolation, and connection and resource management to enable some degree of scaling up to use-cases like internal dashboards.
|
|
24
|
+
|
|
25
|
+
It can be as easy as:
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
import mywebframework
|
|
29
|
+
from mplbed import mplbed_mywebframework
|
|
30
|
+
|
|
31
|
+
@mywebframework.route("/myplot")
|
|
32
|
+
@mplbed_mywebframework.figure_page
|
|
33
|
+
def my_plot_page(request):
|
|
34
|
+
fig, ax = plt.subplots()
|
|
35
|
+
ax.plot([1, 2, 3], [4, 5, 6])
|
|
36
|
+
return fig
|
|
37
|
+
|
|
38
|
+
app = mywebframework.App(...)
|
|
39
|
+
setup(app)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Currently you can install this package from Github:
|
|
45
|
+
|
|
46
|
+
$ uv add git+https://github.com/frankier/mplbed
|
|
47
|
+
|
|
48
|
+
## Documentation
|
|
49
|
+
|
|
50
|
+
The documentation includes [API docs](https://frankier.github.io/mplbed/api.html) and [examples](https://frankier.github.io/mplbed/examples.html).
|
|
51
|
+
|
|
52
|
+
## Contributing
|
|
53
|
+
|
|
54
|
+
Contributions are welcome. Please discuss any larger changes in the issues
|
|
55
|
+
before making a pull request to avoid wasted work. You, the human contributor,
|
|
56
|
+
must personally have reviewed and understood any code submitted.
|
|
57
|
+
|
|
58
|
+
Set up a dev environment with [uv](https://docs.astral.sh/uv/):
|
|
59
|
+
|
|
60
|
+
$ uv sync --all-groups --all-extras
|
|
61
|
+
|
|
62
|
+
Run the tests (end-to-end tests need Playwright browsers: `uv run playwright install chromium`):
|
|
63
|
+
|
|
64
|
+
$ uv run pytest
|
|
65
|
+
|
|
66
|
+
Lint, format and typecheck:
|
|
67
|
+
|
|
68
|
+
$ uv run ruff check ./src
|
|
69
|
+
$ uv run ruff format ./src
|
|
70
|
+
$ uv run ty check ./src
|
|
71
|
+
|
|
72
|
+
Install the [prek](https://prek.j178.dev/) hook to run these automatically on commit:
|
|
73
|
+
|
|
74
|
+
$ prek install
|
mplbed-0.1.0/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# mplbed
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
|[]mpl
|
|
5
|
+
|======|
|
|
6
|
+
| bed |
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`mplbed` is a library of support code for embedding interactive, server-side matplotlib figures in web applications.
|
|
10
|
+
|
|
11
|
+
Supporting, in principle any ASGI-compatible Python application, it provides convenient integration for a number of frameworks ones out of the box.
|
|
12
|
+
It aims to have "usually-works" defaults useful for quick demos, while providing a lot of flexibility and options as well as utilities to deal with advanced issues such as style isolation, and connection and resource management to enable some degree of scaling up to use-cases like internal dashboards.
|
|
13
|
+
|
|
14
|
+
It can be as easy as:
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
import mywebframework
|
|
18
|
+
from mplbed import mplbed_mywebframework
|
|
19
|
+
|
|
20
|
+
@mywebframework.route("/myplot")
|
|
21
|
+
@mplbed_mywebframework.figure_page
|
|
22
|
+
def my_plot_page(request):
|
|
23
|
+
fig, ax = plt.subplots()
|
|
24
|
+
ax.plot([1, 2, 3], [4, 5, 6])
|
|
25
|
+
return fig
|
|
26
|
+
|
|
27
|
+
app = mywebframework.App(...)
|
|
28
|
+
setup(app)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
Currently you can install this package from Github:
|
|
34
|
+
|
|
35
|
+
$ uv add git+https://github.com/frankier/mplbed
|
|
36
|
+
|
|
37
|
+
## Documentation
|
|
38
|
+
|
|
39
|
+
The documentation includes [API docs](https://frankier.github.io/mplbed/api.html) and [examples](https://frankier.github.io/mplbed/examples.html).
|
|
40
|
+
|
|
41
|
+
## Contributing
|
|
42
|
+
|
|
43
|
+
Contributions are welcome. Please discuss any larger changes in the issues
|
|
44
|
+
before making a pull request to avoid wasted work. You, the human contributor,
|
|
45
|
+
must personally have reviewed and understood any code submitted.
|
|
46
|
+
|
|
47
|
+
Set up a dev environment with [uv](https://docs.astral.sh/uv/):
|
|
48
|
+
|
|
49
|
+
$ uv sync --all-groups --all-extras
|
|
50
|
+
|
|
51
|
+
Run the tests (end-to-end tests need Playwright browsers: `uv run playwright install chromium`):
|
|
52
|
+
|
|
53
|
+
$ uv run pytest
|
|
54
|
+
|
|
55
|
+
Lint, format and typecheck:
|
|
56
|
+
|
|
57
|
+
$ uv run ruff check ./src
|
|
58
|
+
$ uv run ruff format ./src
|
|
59
|
+
$ uv run ty check ./src
|
|
60
|
+
|
|
61
|
+
Install the [prek](https://prek.j178.dev/) hook to run these automatically on commit:
|
|
62
|
+
|
|
63
|
+
$ prek install
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "mplbed"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Integrate Matplotlib's WebAgg backend with Starlette for real-time plotting in web applications."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"frozendict>=2.4.7",
|
|
9
|
+
"matplotlib>=3.10.9",
|
|
10
|
+
"starlette>=1.0.0",
|
|
11
|
+
"tornado>=6.5.5",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
demo = [
|
|
16
|
+
"mne>=1.12.1",
|
|
17
|
+
"quart",
|
|
18
|
+
"django",
|
|
19
|
+
"daphne",
|
|
20
|
+
"questionary"
|
|
21
|
+
]
|
|
22
|
+
docs = [
|
|
23
|
+
"sphinx>=8",
|
|
24
|
+
"myst-parser>=4",
|
|
25
|
+
"sphinx-autodoc-typehints>=2",
|
|
26
|
+
"furo>=2024",
|
|
27
|
+
"quart",
|
|
28
|
+
]
|
|
29
|
+
test = [
|
|
30
|
+
"pytest>=8",
|
|
31
|
+
"pytest-playwright>=0.5",
|
|
32
|
+
"pillow>=10",
|
|
33
|
+
]
|
|
34
|
+
qa = [
|
|
35
|
+
"ty",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.pytest.ini_options]
|
|
39
|
+
testpaths = ["tests"]
|
|
40
|
+
markers = [
|
|
41
|
+
"e2e: end-to-end Playwright tests that drive a real browser",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.ruff]
|
|
45
|
+
line-length = 120
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint]
|
|
48
|
+
select = [
|
|
49
|
+
# pycodestyle
|
|
50
|
+
"E",
|
|
51
|
+
# Pyflakes
|
|
52
|
+
"F",
|
|
53
|
+
# pyupgrade
|
|
54
|
+
"UP",
|
|
55
|
+
# flake8-bugbear
|
|
56
|
+
"B",
|
|
57
|
+
# flake8-simplify
|
|
58
|
+
"SIM",
|
|
59
|
+
# isort
|
|
60
|
+
"I",
|
|
61
|
+
# pyupgrade
|
|
62
|
+
"UP",
|
|
63
|
+
# pydocstyle
|
|
64
|
+
"D",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
ignore = [
|
|
68
|
+
# Package/module-level docstrings
|
|
69
|
+
"D100",
|
|
70
|
+
"D104",
|
|
71
|
+
# Summary line seprate from body
|
|
72
|
+
"D205"
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint.pydocstyle]
|
|
76
|
+
convention = "numpy"
|
|
77
|
+
ignore-decorators = ["mplbed._doc_helpers.doc", "mplbed.integration._common.setup_page_docstring"]
|
|
78
|
+
|
|
79
|
+
[build-system]
|
|
80
|
+
requires = ["uv_build >= 0.11.7, <0.12.0"]
|
|
81
|
+
build-backend = "uv_build"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from .asgi import MplbedMiddleware
|
|
2
|
+
from .html import raw as raw_html
|
|
3
|
+
from .html import safe as safe_html
|
|
4
|
+
from .integration import quart as mplbed_quart
|
|
5
|
+
from .integration import starlette as mplbed_starlette
|
|
6
|
+
from .server import mplbed_app_factory
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"mplbed_quart",
|
|
10
|
+
"mplbed_starlette",
|
|
11
|
+
"mplbed_app_factory",
|
|
12
|
+
"raw_html",
|
|
13
|
+
"safe_html",
|
|
14
|
+
"MplbedMiddleware",
|
|
15
|
+
]
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .consts import DEFAULT_PREFIX
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def doc(s):
|
|
7
|
+
def decorator(func):
|
|
8
|
+
func.__doc__ = s
|
|
9
|
+
return func
|
|
10
|
+
|
|
11
|
+
return decorator
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def fdf(s):
|
|
15
|
+
"""_F_ix _D_ocstring _F_ragment for inclusion in a docstring template."""
|
|
16
|
+
from inspect import cleandoc
|
|
17
|
+
from textwrap import indent
|
|
18
|
+
|
|
19
|
+
return indent(cleandoc(s), " ").strip()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class DotAccessDict(dict[str, Any]):
|
|
23
|
+
__getattr__ = dict.get
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PARAMS_DS: Any = DotAccessDict(
|
|
27
|
+
prefix=fdf(f"""
|
|
28
|
+
prefix : str, optional
|
|
29
|
+
The URL prefix for the routes handleded by `mplbed`. Default is '{DEFAULT_PREFIX}'.
|
|
30
|
+
"""),
|
|
31
|
+
mplbed_starlette_app=lambda name="mplbed_starlette_app": fdf(f"""
|
|
32
|
+
{name} : Starlette, optional
|
|
33
|
+
The Starlette app to use for the `mplbed` routes, as returned by `mplbed_starlette_app_factory`.
|
|
34
|
+
If not provided, a new app will be created using the provided `mplbed_starlette_app_kwargs`.
|
|
35
|
+
"""),
|
|
36
|
+
mplbed_starlette_app_kwargs=lambda name="mplbed_starlette_app_kwargs": fdf(f"""
|
|
37
|
+
{name} : dict, optional
|
|
38
|
+
Keyword arguments to pass to the Mplbed app factory if `mplbed_starlette_app` is not provided.
|
|
39
|
+
"""),
|
|
40
|
+
manage_routing=fdf("""
|
|
41
|
+
manage_routing : bool, optional
|
|
42
|
+
Whether the ASGI middleware should manage routing. Default is True.
|
|
43
|
+
If you set this to False, you are responsible for routing requests to
|
|
44
|
+
the Mplbed app under the given `prefix`.
|
|
45
|
+
"""),
|
|
46
|
+
native_app=fdf("""
|
|
47
|
+
native_app : Any, optional
|
|
48
|
+
The native app, e.g. the `Starlette` or `Quart` instance, which will
|
|
49
|
+
typically be saved in cased it is needed by the specific integration,
|
|
50
|
+
e.g. for rendering templates.
|
|
51
|
+
"""),
|
|
52
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from contextvars import ContextVar
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from starlette.applications import Starlette
|
|
5
|
+
from starlette.routing import Match, Mount
|
|
6
|
+
|
|
7
|
+
from mplbed._doc_helpers import PARAMS_DS as D
|
|
8
|
+
from mplbed._doc_helpers import doc
|
|
9
|
+
|
|
10
|
+
_native_app: ContextVar[Any] = ContextVar("_native_app", default=None)
|
|
11
|
+
_prefix_and_app: ContextVar[tuple[str, Starlette] | None] = ContextVar("_prefix_and_app", default=None)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class MplbedMiddleware:
|
|
15
|
+
"""
|
|
16
|
+
ASGI middleware to sets up routing and ensures context the correct context is
|
|
17
|
+
available so that the integrations work correctly.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@doc(
|
|
21
|
+
f"""
|
|
22
|
+
Initialize the MplbedMiddleware.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
default_app : ASGI3Application
|
|
27
|
+
The default ASGI app to use (i.e. the main app, typically yours).
|
|
28
|
+
{D.prefix}
|
|
29
|
+
{D.mplbed_starlette_app("app")}
|
|
30
|
+
{D.mplbed_starlette_app_kwargs("app_kwargs")}
|
|
31
|
+
{D.manage_routing}
|
|
32
|
+
{D.native_app}
|
|
33
|
+
"""
|
|
34
|
+
)
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
default_app,
|
|
38
|
+
*,
|
|
39
|
+
prefix: str,
|
|
40
|
+
app=None,
|
|
41
|
+
app_kwargs=None,
|
|
42
|
+
manage_routing=True,
|
|
43
|
+
native_app=None,
|
|
44
|
+
):
|
|
45
|
+
from mplbed.server import mplbed_app_factory
|
|
46
|
+
|
|
47
|
+
self.main_app = default_app
|
|
48
|
+
if app is None and not manage_routing:
|
|
49
|
+
raise ValueError(
|
|
50
|
+
"If manage_routing is False, you must construct and provide the app yourself "
|
|
51
|
+
"(otherwise how do you plan to route to it?)"
|
|
52
|
+
)
|
|
53
|
+
if app is not None:
|
|
54
|
+
self.mplbed_app = app
|
|
55
|
+
elif app_kwargs is not None:
|
|
56
|
+
self.mplbed_app = mplbed_app_factory(**app_kwargs)
|
|
57
|
+
else:
|
|
58
|
+
self.mplbed_app = mplbed_app_factory()
|
|
59
|
+
self.prefix = prefix
|
|
60
|
+
self.manage_routing = manage_routing
|
|
61
|
+
self.native_app = native_app
|
|
62
|
+
if manage_routing:
|
|
63
|
+
self.mount = Mount(prefix, self.mplbed_app)
|
|
64
|
+
|
|
65
|
+
async def __call__(self, scope, receive, send):
|
|
66
|
+
"""ASGI entry point for the middleware."""
|
|
67
|
+
with (
|
|
68
|
+
_native_app.set(self.native_app),
|
|
69
|
+
_prefix_and_app.set((self.prefix, self.mplbed_app)),
|
|
70
|
+
):
|
|
71
|
+
if self.manage_routing:
|
|
72
|
+
assert self.mount
|
|
73
|
+
match, child_scope = self.mount.matches(scope)
|
|
74
|
+
if match != Match.NONE:
|
|
75
|
+
scope.update(child_scope)
|
|
76
|
+
await self.mplbed_app(scope, receive, send)
|
|
77
|
+
return
|
|
78
|
+
await self.main_app(scope, receive, send)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def get_native_app():
|
|
82
|
+
"""Get the native app from the current context.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
Any
|
|
87
|
+
The native app, or None if not set.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
return _native_app.get()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_asgi_app():
|
|
94
|
+
"""Get the ASGI app from the current context.
|
|
95
|
+
|
|
96
|
+
Returns
|
|
97
|
+
-------
|
|
98
|
+
ASGI3Application | None
|
|
99
|
+
The ASGI app, or None if not set.
|
|
100
|
+
|
|
101
|
+
"""
|
|
102
|
+
val = _prefix_and_app.get()
|
|
103
|
+
if val is not None:
|
|
104
|
+
_, app = val
|
|
105
|
+
return app
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def url_path_for(name, **path_params):
|
|
110
|
+
"""Get the URL path for a given route name and parameters."""
|
|
111
|
+
prefix_and_app = path_params.pop("_prefix_and_app", None)
|
|
112
|
+
if prefix_and_app is None:
|
|
113
|
+
prefix_and_app = _prefix_and_app.get()
|
|
114
|
+
if prefix_and_app is None:
|
|
115
|
+
raise RuntimeError(
|
|
116
|
+
"Missing current prefix_and_app in context! "
|
|
117
|
+
"Did you install the MlpbedMiddleware? (_prefix_and_app was not passed)"
|
|
118
|
+
)
|
|
119
|
+
prefix, app = prefix_and_app
|
|
120
|
+
path = app.url_path_for(name, **path_params)
|
|
121
|
+
return prefix + path
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from mplbed.asgi import url_path_for
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def figure_html_from_id(fig_id, *, target="inline", on_close="msg_discrete", prefix_and_app=None):
|
|
5
|
+
from json import dumps
|
|
6
|
+
|
|
7
|
+
ws_uri = url_path_for("websocket", fig_id=fig_id, _prefix_and_app=prefix_and_app)
|
|
8
|
+
ws_uri_str = dumps(ws_uri)
|
|
9
|
+
download_fig_uri = url_path_for("download_fig", fig_id=fig_id, fmt="{fmt}", _prefix_and_app=prefix_and_app)
|
|
10
|
+
download_fig_uri_str = dumps(download_fig_uri)
|
|
11
|
+
container = ""
|
|
12
|
+
setup_container = ""
|
|
13
|
+
if target == "inline":
|
|
14
|
+
container = "<div></div>"
|
|
15
|
+
target_js = "document.currentScript.previousElementSibling"
|
|
16
|
+
elif target == "body":
|
|
17
|
+
target_js = "document.body"
|
|
18
|
+
elif target == "modal":
|
|
19
|
+
container = """
|
|
20
|
+
<dialog closedby="any" style="padding: 1em; margin: 0 auto;"></dialog>
|
|
21
|
+
""".strip()
|
|
22
|
+
target_js = "document.currentScript.previousElementSibling"
|
|
23
|
+
setup_container = """
|
|
24
|
+
_mpl_webaggext.mk_modal(document.currentScript.previousElementSibling, fig);
|
|
25
|
+
""".strip()
|
|
26
|
+
else:
|
|
27
|
+
raise ValueError(f"Invalid target: {target}")
|
|
28
|
+
if on_close == "remove_dialog":
|
|
29
|
+
on_close = ["remove_parent", "dialog"]
|
|
30
|
+
on_close_js = dumps(on_close)
|
|
31
|
+
create_figure = f"""
|
|
32
|
+
let fig = _mpl_webaggext.new_fig(
|
|
33
|
+
{target_js},
|
|
34
|
+
{fig_id},
|
|
35
|
+
{ws_uri_str},
|
|
36
|
+
{download_fig_uri_str},
|
|
37
|
+
{on_close_js}
|
|
38
|
+
);
|
|
39
|
+
""".strip()
|
|
40
|
+
bits = (
|
|
41
|
+
container,
|
|
42
|
+
"""
|
|
43
|
+
<script>
|
|
44
|
+
(function() {
|
|
45
|
+
""".strip(),
|
|
46
|
+
create_figure,
|
|
47
|
+
setup_container,
|
|
48
|
+
"""
|
|
49
|
+
})();
|
|
50
|
+
</script>
|
|
51
|
+
""".strip(),
|
|
52
|
+
)
|
|
53
|
+
return "\n".join(bits)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def figure_html(figure, *retains, target="inline", on_close="msg_discrete"):
|
|
57
|
+
from matplotlib.pyplot import _get_backend_mod as get_backend_mod
|
|
58
|
+
|
|
59
|
+
from mplbed.server._impl import add_manager
|
|
60
|
+
|
|
61
|
+
manager = get_backend_mod().new_figure_manager_given_figure(id(figure), figure)
|
|
62
|
+
if hasattr(manager, "add_retains"):
|
|
63
|
+
manager.add_retains(*retains) # ty: ignore
|
|
64
|
+
else:
|
|
65
|
+
if not hasattr(figure, "_retains"):
|
|
66
|
+
figure._retains = []
|
|
67
|
+
figure._retains.extend(retains)
|
|
68
|
+
add_manager(manager)
|
|
69
|
+
return figure_html_from_id(manager.num, target=target, on_close=on_close)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def default_figure_page_template(*, head, fig, title):
|
|
73
|
+
return f"""<!DOCTYPE html>
|
|
74
|
+
<html lang="en">
|
|
75
|
+
<head>
|
|
76
|
+
{head}
|
|
77
|
+
<title>{title}</title>
|
|
78
|
+
</head>
|
|
79
|
+
<body>
|
|
80
|
+
{fig}
|
|
81
|
+
</body>
|
|
82
|
+
</html>
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
_template_preview = default_figure_page_template(head="{head}", fig="{fig}", title="{title}")
|
|
87
|
+
_indented = "\n".join(" " + line for line in _template_preview.splitlines())
|
|
88
|
+
default_figure_page_template.__doc__ = (
|
|
89
|
+
"Applies the following template to the given ``head``, ``fig`` and ``title``:\n\n"
|
|
90
|
+
".. code-block:: html\n\n" + _indented + "\n"
|
|
91
|
+
)
|
|
92
|
+
del _template_preview, _indented
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def head_content(*, core=False, prefix_and_app=None):
|
|
96
|
+
css_files = []
|
|
97
|
+
if not core:
|
|
98
|
+
css_files.extend(["page", "boilerplate", "fbm"])
|
|
99
|
+
css_files.append("mpl")
|
|
100
|
+
head_bits = []
|
|
101
|
+
for css_file in css_files:
|
|
102
|
+
static_url = url_path_for("static", path=f"css/{css_file}.css", _prefix_and_app=prefix_and_app)
|
|
103
|
+
head_bits.append(
|
|
104
|
+
f"""
|
|
105
|
+
<link rel="stylesheet" href="{static_url}" type="text/css">
|
|
106
|
+
""".strip()
|
|
107
|
+
)
|
|
108
|
+
mpl_js_uri = url_path_for("mpl_js", _prefix_and_app=prefix_and_app)
|
|
109
|
+
head_bits.append(
|
|
110
|
+
f"""
|
|
111
|
+
<script src="{mpl_js_uri}"></script>
|
|
112
|
+
""".strip()
|
|
113
|
+
)
|
|
114
|
+
webaggext_js_uri = url_path_for("webaggext_js", _prefix_and_app=prefix_and_app)
|
|
115
|
+
head_bits.append(
|
|
116
|
+
f"""
|
|
117
|
+
<script src="{webaggext_js_uri}"></script>
|
|
118
|
+
""".strip()
|
|
119
|
+
)
|
|
120
|
+
head_bits.append(
|
|
121
|
+
"""
|
|
122
|
+
<style>
|
|
123
|
+
.mpl-toolbar {
|
|
124
|
+
position: relative;
|
|
125
|
+
padding-bottom: 2em;
|
|
126
|
+
}
|
|
127
|
+
.mpl-message {
|
|
128
|
+
position: absolute;
|
|
129
|
+
left: 0;
|
|
130
|
+
bottom: 0;
|
|
131
|
+
white-space: nowrap;
|
|
132
|
+
overflow-x: auto;
|
|
133
|
+
width: 100%;
|
|
134
|
+
}
|
|
135
|
+
.mpl-figure-root {
|
|
136
|
+
display: inline-flex !important;
|
|
137
|
+
flex-direction: column;
|
|
138
|
+
}
|
|
139
|
+
</style>
|
|
140
|
+
""".strip()
|
|
141
|
+
)
|
|
142
|
+
return "\n".join(head_bits)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def figure_page_html(fig, *, template=default_figure_page_template):
|
|
146
|
+
fig_html = figure_html(fig, target="body")
|
|
147
|
+
head = head_content(core=True)
|
|
148
|
+
resp_html = template(head=head, title="figure", fig=fig_html)
|
|
149
|
+
return resp_html
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from mplbed.html._impl import (
|
|
2
|
+
default_figure_page_template,
|
|
3
|
+
figure_html,
|
|
4
|
+
figure_html_from_id,
|
|
5
|
+
figure_page_html,
|
|
6
|
+
head_content,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"figure_html_from_id",
|
|
11
|
+
"figure_html",
|
|
12
|
+
"head_content",
|
|
13
|
+
"figure_page_html",
|
|
14
|
+
"default_figure_page_template",
|
|
15
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from mplbed.html import _impl
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _wrap_markupsafe(func):
|
|
5
|
+
def wrapper(*args, **kwargs):
|
|
6
|
+
from markupsafe import Markup
|
|
7
|
+
|
|
8
|
+
return Markup(func(*args, **kwargs))
|
|
9
|
+
|
|
10
|
+
return wrapper
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
figure_html_from_id = _wrap_markupsafe(_impl.figure_html_from_id)
|
|
14
|
+
head_content = _wrap_markupsafe(_impl.head_content)
|
|
15
|
+
figure_html = _wrap_markupsafe(_impl.figure_html)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"figure_html_from_id",
|
|
20
|
+
"figure_html",
|
|
21
|
+
"head_content",
|
|
22
|
+
]
|