anyplotlib 0.1.0__py3-none-any.whl

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.
Files changed (42) hide show
  1. anyplotlib/__init__.py +53 -0
  2. anyplotlib/_base_plot.py +229 -0
  3. anyplotlib/_electron.py +74 -0
  4. anyplotlib/_repr_utils.py +345 -0
  5. anyplotlib/_utils.py +194 -0
  6. anyplotlib/axes/__init__.py +6 -0
  7. anyplotlib/axes/_axes.py +510 -0
  8. anyplotlib/axes/_inset_axes.py +126 -0
  9. anyplotlib/callbacks.py +328 -0
  10. anyplotlib/embed.py +194 -0
  11. anyplotlib/figure/__init__.py +7 -0
  12. anyplotlib/figure/_figure.py +633 -0
  13. anyplotlib/figure/_gridspec.py +99 -0
  14. anyplotlib/figure/_subplots.py +100 -0
  15. anyplotlib/figure_esm.js +6011 -0
  16. anyplotlib/markers.py +704 -0
  17. anyplotlib/plot1d/__init__.py +6 -0
  18. anyplotlib/plot1d/_plot1d.py +1376 -0
  19. anyplotlib/plot1d/_plotbar.py +436 -0
  20. anyplotlib/plot2d/__init__.py +5 -0
  21. anyplotlib/plot2d/_plot2d.py +726 -0
  22. anyplotlib/plot2d/_plotmesh.py +116 -0
  23. anyplotlib/plot3d/__init__.py +4 -0
  24. anyplotlib/plot3d/_plot3d.py +524 -0
  25. anyplotlib/plotxy/__init__.py +4 -0
  26. anyplotlib/plotxy/_plotxy.py +273 -0
  27. anyplotlib/sphinx_anywidget/__init__.py +177 -0
  28. anyplotlib/sphinx_anywidget/_directive.py +245 -0
  29. anyplotlib/sphinx_anywidget/_repr_utils.py +298 -0
  30. anyplotlib/sphinx_anywidget/_scraper.py +390 -0
  31. anyplotlib/sphinx_anywidget/_wheel_builder.py +84 -0
  32. anyplotlib/sphinx_anywidget/static/anywidget_bridge.js +1099 -0
  33. anyplotlib/sphinx_anywidget/static/anywidget_overlay.css +100 -0
  34. anyplotlib/widgets/__init__.py +18 -0
  35. anyplotlib/widgets/_base.py +218 -0
  36. anyplotlib/widgets/_widgets1d.py +109 -0
  37. anyplotlib/widgets/_widgets2d.py +141 -0
  38. anyplotlib/widgets/_widgets3d.py +50 -0
  39. anyplotlib-0.1.0.dist-info/METADATA +160 -0
  40. anyplotlib-0.1.0.dist-info/RECORD +42 -0
  41. anyplotlib-0.1.0.dist-info/WHEEL +4 -0
  42. anyplotlib-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,245 @@
1
+ """
2
+ sphinx_anywidget/_directive.py
3
+ ================================
4
+
5
+ RST directive for embedding interactive anywidget figures directly in ``.rst``
6
+ pages — no Sphinx Gallery required.
7
+
8
+ Usage
9
+ -----
10
+
11
+ Static snapshot only::
12
+
13
+ .. anywidget-figure:: ../Examples/plot_image2d.py
14
+
15
+ Interactive (Pyodide-activatable)::
16
+
17
+ .. anywidget-figure:: ../Examples/plot_image2d.py
18
+ :interactive:
19
+
20
+ Options
21
+ -------
22
+ ``:interactive:``
23
+ Flag. When present the ⚡ activation badge is shown and the example
24
+ source is embedded for live re-execution by the Pyodide bridge.
25
+ ``:width:`` (int, default 684)
26
+ Maximum display width in pixels.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import hashlib
32
+ import json as _json
33
+ import re
34
+ import runpy
35
+ import tempfile
36
+ from html import escape as _html_escape
37
+ from pathlib import Path
38
+
39
+ # Reuse the regex parsers from the scraper.
40
+ from anyplotlib.sphinx_anywidget._scraper import (
41
+ _PYODIDE_PACKAGES_RE,
42
+ _PYODIDE_MOCK_PACKAGES_RE,
43
+ )
44
+
45
+ from docutils import nodes
46
+ from docutils.parsers.rst import Directive, directives
47
+
48
+
49
+ class AnywidgetFigureDirective(Directive):
50
+ """Directive: ``.. anywidget-figure:: path/to/script.py``."""
51
+
52
+ required_arguments = 1 # path to the Python source file
53
+ optional_arguments = 0
54
+ has_content = False
55
+ option_spec = {
56
+ "interactive": directives.flag,
57
+ "width": directives.unchanged, # e.g. "684", "100%", "80%"
58
+ "height": directives.unchanged, # e.g. "400px", "400"
59
+ }
60
+
61
+ def run(self):
62
+ env = self.state.document.settings.env
63
+ config = env.config
64
+
65
+ # ── resolve the source file path ─────────────────────────────────
66
+ src_arg = self.arguments[0]
67
+ # env.confdir was removed in Sphinx 9; fall back to env.app.confdir
68
+ # then env.srcdir so the directive works on all supported versions.
69
+ conf_dir = Path(
70
+ getattr(env, "confdir", None)
71
+ or getattr(env.app, "confdir", None)
72
+ or env.srcdir
73
+ )
74
+ src_path = (conf_dir / src_arg).resolve()
75
+
76
+ if not src_path.exists():
77
+ error = self.reporter.error(
78
+ f"anywidget-figure: source file not found: {src_path}",
79
+ nodes.literal_block(src_arg, src_arg),
80
+ line=self.lineno,
81
+ )
82
+ return [error]
83
+
84
+ # ── options ──────────────────────────────────────────────────────
85
+ is_interactive = "interactive" in self.options
86
+
87
+ # :width: accepts "684" (pixels) or "100%" / "80%" (percentage).
88
+ # A percentage means "use the full container width" — we pass
89
+ # max_width=None so _iframe_html uses its default MAX_DOC_WIDTH cap
90
+ # and the CSS wrapper fills the container via its inline-block rule.
91
+ max_width = None
92
+ raw_width = self.options.get("width", None)
93
+ if raw_width is not None:
94
+ raw_width = str(raw_width).strip()
95
+ if not raw_width.endswith("%"):
96
+ try:
97
+ max_width = int(raw_width.replace("px", "").strip())
98
+ except (ValueError, TypeError):
99
+ max_width = None
100
+ # else: percentage → leave max_width=None (full-width default)
101
+
102
+ # :height: accepts "400px" or "400".
103
+ max_height = None
104
+ raw_height = self.options.get("height", None)
105
+ if raw_height is not None:
106
+ try:
107
+ max_height = int(str(raw_height).lower().replace("px", "").strip())
108
+ except (ValueError, TypeError):
109
+ max_height = None
110
+
111
+ # ── execute the script to get the widget ─────────────────────────
112
+ try:
113
+ g = runpy.run_path(str(src_path), run_name="__main__")
114
+ except Exception as exc:
115
+ error = self.reporter.error(
116
+ f"anywidget-figure: failed to execute {src_path.name}: {exc}",
117
+ nodes.literal_block(str(exc), str(exc)),
118
+ line=self.lineno,
119
+ )
120
+ return [error]
121
+
122
+ widget = _find_widget(g)
123
+ if widget is None:
124
+ error = self.reporter.error(
125
+ f"anywidget-figure: no anywidget found in {src_path.name}",
126
+ line=self.lineno,
127
+ )
128
+ return [error]
129
+
130
+ # ── write the standalone HTML file ───────────────────────────────
131
+ from anyplotlib.sphinx_anywidget._repr_utils import (
132
+ build_standalone_html, _widget_px,
133
+ )
134
+ from anyplotlib.sphinx_anywidget._scraper import _iframe_html
135
+
136
+ # Use a stable ID derived from the *full* resolved path so two files
137
+ # with the same basename don't overwrite each other or share a fig_id.
138
+ path_hash = hashlib.md5(str(src_path).encode()).hexdigest()[:12]
139
+ fig_id = f"rst_{path_hash}"
140
+
141
+ # Write the standalone HTML directly into the Sphinx output _static dir
142
+ # so the relative URL we embed in the RST resolves correctly.
143
+ out_dir = Path(env.app.outdir)
144
+ docs_static = out_dir / "_static" / "viewer_widgets"
145
+ docs_static.mkdir(parents=True, exist_ok=True)
146
+ html_name = f"{fig_id}.html"
147
+ html_path = docs_static / html_name
148
+
149
+ inner_html = build_standalone_html(widget, resizable=False, fig_id=fig_id)
150
+ html_path.write_text(inner_html, encoding="utf-8")
151
+
152
+ w, h = _widget_px(widget)
153
+
154
+ # Compute relative path from the current RST file's output dir
155
+ # to _static/viewer_widgets/.
156
+ # doc_name is the docname without extension, e.g. "index" or "dev/index".
157
+ # The output HTML sits at {out_dir}/{doc_name}.html, so the number of
158
+ # "../" hops needed to reach {out_dir}/_static/ equals the depth of the
159
+ # *directory* part of the docname (not the filename itself).
160
+ try:
161
+ doc_name = env.docname # e.g. "index" or "dev/index"
162
+ rel_depth = len(Path(doc_name).parent.parts) # parent dirs only
163
+ prefix = "../" * rel_depth
164
+ except Exception:
165
+ prefix = ""
166
+
167
+ src_url = f"{prefix}_static/viewer_widgets/{html_name}"
168
+ iframe_kw = {}
169
+ if max_width is not None:
170
+ iframe_kw["max_width"] = max_width
171
+ if max_height is not None:
172
+ iframe_kw["max_height"] = max_height
173
+ iframe_block = _iframe_html(
174
+ src_url, w, h,
175
+ fig_id=fig_id,
176
+ interactive=is_interactive,
177
+ **iframe_kw,
178
+ )
179
+
180
+ raw_html = "\n" + iframe_block + "\n"
181
+
182
+ if is_interactive:
183
+ python_src = ""
184
+ try:
185
+ python_src = src_path.read_text(encoding="utf-8")
186
+ except Exception:
187
+ pass
188
+
189
+ if python_src:
190
+ data_src = _html_escape(_json.dumps(python_src), quote=True)
191
+
192
+ # Detect _PYODIDE_PACKAGES = [...] in the source.
193
+ _pkg_attr = ""
194
+ m = _PYODIDE_PACKAGES_RE.search(python_src)
195
+ if m:
196
+ try:
197
+ import ast as _ast
198
+ pkgs = _ast.literal_eval(m.group(1))
199
+ if pkgs:
200
+ _pkg_attr = (
201
+ f' data-pyodide-packages='
202
+ f'"{_html_escape(_json.dumps(pkgs), quote=True)}"'
203
+ )
204
+ except Exception:
205
+ pass
206
+
207
+ # Detect _PYODIDE_MOCK_PACKAGES = [...] in the source.
208
+ _mock_attr = ""
209
+ m2 = _PYODIDE_MOCK_PACKAGES_RE.search(python_src)
210
+ if m2:
211
+ try:
212
+ import ast as _ast2
213
+ mock_pkgs = _ast2.literal_eval(m2.group(1))
214
+ if mock_pkgs:
215
+ _mock_attr = (
216
+ f' data-pyodide-mock-packages='
217
+ f'"{_html_escape(_json.dumps(mock_pkgs), quote=True)}"'
218
+ )
219
+ except Exception:
220
+ pass
221
+
222
+ stem = src_path.stem
223
+ script_tag = (
224
+ f'<script type="text/x-python"'
225
+ f' data-fig-id="{fig_id}"'
226
+ f' data-fig-index="0"'
227
+ f' data-src-file="{stem}"'
228
+ f'{_pkg_attr}'
229
+ f'{_mock_attr}'
230
+ f' data-src="{data_src}"></script>'
231
+ )
232
+ raw_html += "\n" + script_tag + "\n"
233
+
234
+ return [nodes.raw("", raw_html, format="html")]
235
+
236
+
237
+ def _find_widget(globals_dict: dict):
238
+ """Locate the most-recently created anywidget in *globals_dict*."""
239
+ for val in reversed(list(globals_dict.values())):
240
+ if not callable(getattr(val, "_repr_html_", None)):
241
+ continue
242
+ if hasattr(val, "_esm") and hasattr(val, "traits"):
243
+ return val
244
+ return None
245
+
@@ -0,0 +1,298 @@
1
+ """
2
+ sphinx_anywidget/_repr_utils.py
3
+ ================================
4
+
5
+ Self-contained HTML builder for any ``anywidget.AnyWidget`` subclass.
6
+ No runtime dependency on anyplotlib or any specific widget library.
7
+
8
+ Strategy
9
+ --------
10
+ 1. Serialise every ``sync=True`` traitlet to a plain JSON dict.
11
+ 2. Embed that dict and the widget's ``_esm`` source directly in the page.
12
+ 3. Provide a minimal model shim (get/set/on/save_changes) so the ESM's
13
+ render() function works without any Jupyter comm infrastructure.
14
+ 4. Import the ESM as a Blob URL and call ``render({ model, el })``.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from html import escape
21
+ from uuid import uuid4
22
+
23
+ # Maximum display width (px) for the non-resizable notebook embed.
24
+ MAX_NOTEBOOK_WIDTH = 860
25
+
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # Trait serialisation
29
+ # ---------------------------------------------------------------------------
30
+
31
+ def _widget_state(widget) -> dict:
32
+ """Return a {name: value} dict of every synced traitlet."""
33
+ state: dict = {}
34
+ for name, trait in widget.traits(sync=True).items():
35
+ if name.startswith("_"):
36
+ continue
37
+ raw = getattr(widget, name)
38
+ if isinstance(raw, (bytes, bytearray)):
39
+ import base64
40
+ raw = {"buffer": base64.b64encode(raw).decode("ascii")}
41
+ state[name] = raw
42
+ return state
43
+
44
+
45
+ def _widget_px(widget) -> tuple[int, int]:
46
+ """Return ``(width_px, height_px)`` for any anywidget subclass.
47
+
48
+ Tries common trait names in priority order before falling back to a
49
+ sensible default. Widget authors can override by adding
50
+ ``_display_width`` and ``_display_height`` *non-synced* attributes.
51
+ """
52
+ # Explicit override
53
+ if hasattr(widget, "_display_width") and hasattr(widget, "_display_height"):
54
+ return int(widget._display_width), int(widget._display_height)
55
+
56
+ kind = type(widget).__name__
57
+
58
+ # anyplotlib Figure — gridDiv adds 16 px padding on each side
59
+ if kind == "Figure":
60
+ try:
61
+ return int(widget.fig_width) + 16, int(widget.fig_height) + 16
62
+ except Exception:
63
+ pass
64
+
65
+ # Common viewer patterns: viewer_width / viewer_height traits
66
+ if hasattr(widget, "viewer_width") and hasattr(widget, "viewer_height"):
67
+ return int(widget.viewer_width) + 20, int(widget.viewer_height) + 20
68
+
69
+ # width / height traits
70
+ if hasattr(widget, "width") and hasattr(widget, "height"):
71
+ try:
72
+ return int(widget.width), int(widget.height)
73
+ except Exception:
74
+ pass
75
+
76
+ return 560, 340
77
+
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # HTML builder
81
+ # ---------------------------------------------------------------------------
82
+
83
+ _NO_RESIZE_CSS = """\
84
+ /* ── resizable=False overrides ─────────────────────────────── */
85
+ div[style*="nwse-resize"],
86
+ div[title="Drag to resize"],
87
+ div[title="Drag to resize figure"] {{
88
+ display: none !important;
89
+ }}
90
+ #widget-root > div {{
91
+ padding-bottom: 0 !important;
92
+ padding-right: 0 !important;
93
+ }}
94
+ """
95
+
96
+ _PAGE_TEMPLATE = """\
97
+ <!DOCTYPE html>
98
+ <html>
99
+ <head>
100
+ <meta charset="utf-8"/>
101
+ <style>
102
+ html, body {{
103
+ margin: 0;
104
+ padding: 0;
105
+ background: transparent;
106
+ overflow: hidden;
107
+ width: {width}px;
108
+ height: {height}px;
109
+ }}
110
+ #widget-root {{
111
+ display: inline-block;
112
+ line-height: 0;
113
+ }}
114
+ {extra_css}\
115
+ </style>
116
+ </head>
117
+ <body>
118
+ <div id="widget-root"></div>
119
+ <script type="module">
120
+ const STATE = {state_json};
121
+ // Identifies this iframe to the parent-page anywidget bridge.
122
+ // null → no Pyodide wiring (plain notebook / static docs embed).
123
+ const FIG_ID = {fig_id_json};
124
+
125
+ // Loop-prevention flag: set while applying a parent-originated update so
126
+ // save_changes() doesn't echo the change back as an awi_event.
127
+ let _fromParent = false;
128
+ // Dirty flag: true only when model.set('event_json', ...) was called in the
129
+ // current transaction.
130
+ let _eventJsonDirty = false;
131
+
132
+ function makeModel(state) {{
133
+ const _data = Object.assign({{}}, state);
134
+ const _cbs = {{}};
135
+ const _anyCbs = [];
136
+ return {{
137
+ get(key) {{ return _data[key]; }},
138
+ set(key, val) {{
139
+ _data[key] = val;
140
+ if (key === 'event_json') _eventJsonDirty = true;
141
+ }},
142
+ save_changes() {{
143
+ for (const [ev, cbs] of Object.entries(_cbs))
144
+ for (const cb of cbs) try {{ cb({{ new: _data[ev.slice(7)] }}); }} catch(_) {{}}
145
+ for (const cb of _anyCbs) try {{ cb(); }} catch(_) {{}}
146
+ // Forward interaction events to the parent-page Pyodide instance.
147
+ if (!_fromParent && FIG_ID && window.parent !== window && _eventJsonDirty) {{
148
+ _eventJsonDirty = false;
149
+ try {{
150
+ const ev = JSON.parse(_data.event_json || '{{}}');
151
+ if (ev && ev.source !== 'python') {{
152
+ window.parent.postMessage(
153
+ {{ type: 'awi_event', figId: FIG_ID, data: _data.event_json }}, '*');
154
+ }}
155
+ }} catch(_) {{}}
156
+ }} else {{
157
+ _eventJsonDirty = false;
158
+ }}
159
+ }},
160
+ on(event, cb) {{
161
+ if (event === "change") {{ _anyCbs.push(cb); return; }}
162
+ (_cbs[event] = _cbs[event] || []).push(cb);
163
+ }},
164
+ off(event, cb) {{
165
+ if (!event) {{ for (const k in _cbs) _cbs[k]=[]; _anyCbs.length=0; return; }}
166
+ if (_cbs[event]) _cbs[event] = _cbs[event].filter(c => c !== cb);
167
+ }},
168
+ get model() {{ return this; }},
169
+ }};
170
+ }}
171
+
172
+ const esmSource = {esm_json};
173
+ const blob = new Blob([esmSource], {{ type: "text/javascript" }});
174
+ const blobUrl = URL.createObjectURL(blob);
175
+ const el = document.getElementById("widget-root");
176
+ const model = makeModel(STATE);
177
+
178
+ import(blobUrl).then(mod => {{
179
+ const renderFn = mod.default?.render ?? mod.render;
180
+ if (typeof renderFn === "function") {{
181
+ renderFn({{ model, el }});
182
+ }} else {{
183
+ el.textContent = "ESM has no render() export";
184
+ }}
185
+ }}).catch(err => {{
186
+ el.textContent = "Widget load error: " + err;
187
+ console.error(err);
188
+ }});
189
+
190
+ // ── Inbound state updates from parent-page Pyodide ───────────────────────────
191
+ window.addEventListener('message', (e) => {{
192
+ if (!e.data || e.data.type !== 'awi_state') return;
193
+ _fromParent = true;
194
+ model.set(e.data.key, e.data.value);
195
+ model.save_changes();
196
+ _fromParent = false;
197
+ }});
198
+ </script>
199
+ </body>
200
+ </html>
201
+ """
202
+
203
+
204
+ def build_standalone_html(widget, *, resizable: bool = True,
205
+ fig_id: str | None = None) -> str:
206
+ """Return a self-contained HTML page that renders *widget* interactively.
207
+
208
+ Parameters
209
+ ----------
210
+ widget :
211
+ Any ``anywidget.AnyWidget`` subclass with ``_esm`` defined.
212
+ resizable : bool
213
+ When ``True`` (default) the widget's built-in resize handle is
214
+ preserved. When ``False`` the handle is hidden and the page is
215
+ sized exactly to the widget's natural dimensions.
216
+ fig_id : str or None
217
+ When provided, embedded as ``FIG_ID`` so the parent-page bridge
218
+ can route ``postMessage`` state updates to this iframe.
219
+ """
220
+ state = _widget_state(widget)
221
+
222
+ esm = getattr(widget, "_esm", "") or ""
223
+ if hasattr(esm, "read_text"):
224
+ esm = esm.read_text(encoding="utf-8")
225
+ esm = str(esm)
226
+
227
+ w, h = _widget_px(widget)
228
+ extra_css = _NO_RESIZE_CSS.format() if not resizable else ""
229
+
230
+ return _PAGE_TEMPLATE.format(
231
+ width=w,
232
+ height=h,
233
+ extra_css=extra_css,
234
+ state_json=json.dumps(state, default=str),
235
+ esm_json=json.dumps(esm),
236
+ fig_id_json=json.dumps(fig_id),
237
+ )
238
+
239
+
240
+ def repr_html_iframe(widget, *, resizable: bool = False,
241
+ max_width: int = MAX_NOTEBOOK_WIDTH,
242
+ max_height: int = 800) -> str:
243
+ """Return a centred, responsive ``<iframe srcdoc=...>`` embedding *widget*."""
244
+ inner_html = build_standalone_html(widget, resizable=resizable)
245
+ escaped = escape(inner_html, quote=True)
246
+ uid = str(uuid4()).replace("-", "")
247
+
248
+ w, h = _widget_px(widget)
249
+
250
+ if not resizable:
251
+ init_scale = min(1.0, max_width / w)
252
+ init_w = round(w * init_scale)
253
+ init_h = round(h * init_scale)
254
+ scale_css = f"{init_scale:.6f}".rstrip("0").rstrip(".")
255
+
256
+ js = (
257
+ f"(function(){{"
258
+ f"var wrap=document.getElementById('vw-{uid}'),"
259
+ f"ifr=wrap.querySelector('iframe'),"
260
+ f"nw={w},nh={h};"
261
+ f"function r(){{"
262
+ f"var avail=wrap.parentElement?wrap.parentElement.offsetWidth:0;"
263
+ f"if(!avail)return;"
264
+ f"var s=Math.min(1,avail/nw);"
265
+ f"wrap.style.width=Math.round(nw*s)+'px';"
266
+ f"wrap.style.height=Math.round(nh*s)+'px';"
267
+ f"ifr.style.transform='scale('+s+')';"
268
+ f"}}"
269
+ f"requestAnimationFrame(r);window.addEventListener('resize',r);"
270
+ f"}})()"
271
+ )
272
+
273
+ return (
274
+ f'<div style="display:block;text-align:center;line-height:0;margin:8px 0;">'
275
+ f'<div id="vw-{uid}" style="display:inline-block;overflow:hidden;'
276
+ f'position:relative;width:{init_w}px;height:{init_h}px;">'
277
+ f'<iframe srcdoc="{escaped}" frameborder="0" scrolling="no" '
278
+ f'style="width:{w}px;height:{h}px;border:none;overflow:hidden;display:block;'
279
+ f'transform-origin:top left;transform:scale({scale_css});'
280
+ f'position:absolute;top:0;left:0;">'
281
+ f'</iframe>'
282
+ f'</div>'
283
+ f'<script>{js}</script>'
284
+ f'</div>'
285
+ )
286
+ else:
287
+ return (
288
+ f'<iframe id="vw-{uid}" srcdoc="{escaped}" frameborder="0" '
289
+ f'style="width:100%;height:{h}px;border:none;overflow:hidden;" '
290
+ f'onload="setTimeout(function(){{'
291
+ f'var f=document.getElementById(\'vw-{uid}\');'
292
+ f'if(f&&f.contentWindow&&f.contentWindow.document.body){{'
293
+ f'f.style.height=Math.min('
294
+ f'f.contentWindow.document.body.scrollHeight+20,{max_height})+\'px\''
295
+ f'}}}},'
296
+ f'300)"></iframe>'
297
+ )
298
+