shiny-plotly 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.
@@ -0,0 +1,15 @@
1
+ """Render plotly figures in Shiny for Python without the shinywidgets layer."""
2
+
3
+ from ._deps import __version__, plotly_js, shiny_plotly_js
4
+ from ._html import FIGUREWIDGET_MARGINS, fig_to_ui
5
+ from ._render import output_plotly, render_plotly
6
+
7
+ __all__ = (
8
+ "FIGUREWIDGET_MARGINS",
9
+ "__version__",
10
+ "fig_to_ui",
11
+ "output_plotly",
12
+ "plotly_js",
13
+ "render_plotly",
14
+ "shiny_plotly_js",
15
+ )
shiny_plotly/_deps.py ADDED
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from importlib.metadata import version
4
+
5
+ import plotly
6
+ from htmltools import HTMLDependency
7
+
8
+ __all__ = ("__version__", "plotly_js", "shiny_plotly_js")
9
+
10
+ __version__ = version("shiny-plotly")
11
+
12
+
13
+ def plotly_js() -> HTMLDependency:
14
+ """
15
+ The plotly.js bundle, served by Shiny straight from the installed ``plotly`` wheel.
16
+
17
+ Shiny serves HTML dependencies under ``/lib/<name>-<version>/``, so the URL is keyed
18
+ by the installed plotly version and caches correctly across deploys. Nothing is
19
+ copied or written: the dependency points at ``plotly/package_data/plotly.min.js``,
20
+ the exact bundle ``plotly.offline.get_plotlyjs()`` would inline.
21
+
22
+ Add it once to the page UI so the bundle loads with the page. Every figure rendered by
23
+ :func:`~shiny_plotly.render_plotly` or :func:`~shiny_plotly.fig_to_ui` also carries it,
24
+ so an output still works without the page-level call; htmltools de-duplicates.
25
+ """
26
+ return HTMLDependency(
27
+ name="plotly",
28
+ version=plotly.__version__,
29
+ source={"package": "plotly", "subdir": "package_data"},
30
+ script={"src": "plotly.min.js"},
31
+ )
32
+
33
+
34
+ def shiny_plotly_js() -> HTMLDependency:
35
+ """
36
+ The small browser helper every rendered figure depends on.
37
+
38
+ It keeps each graph sized to its container (plotly alone only reacts to window
39
+ resizes) and purges a graph once Shiny has replaced the output holding it, so
40
+ re-rendering outputs do not accumulate plotly state. It rides along with every
41
+ fragment; there is no need to add it to the page yourself.
42
+ """
43
+ return HTMLDependency(
44
+ name="shiny-plotly",
45
+ version=__version__,
46
+ source={"package": "shiny_plotly", "subdir": "www"},
47
+ script={"src": "shiny-plotly.js"},
48
+ )
shiny_plotly/_html.py ADDED
@@ -0,0 +1,113 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from collections.abc import Mapping
5
+ from types import MappingProxyType
6
+ from typing import Any
7
+
8
+ import plotly.io as pio
9
+ from htmltools import HTML, Tag, TagList, css, tags
10
+ from plotly.basedatatypes import BaseFigure
11
+
12
+ from ._deps import plotly_js, shiny_plotly_js
13
+
14
+ __all__ = ("FIGUREWIDGET_MARGINS", "fig_to_ui")
15
+
16
+ # The margins shinywidgets installs on every plotly FigureWidget it renders (its
17
+ # set_layout_defaults: template.layout.margin = l16/t32/r16/b16), so a migrated app can
18
+ # keep its exact look. Plotly's own defaults are l80/t100/r80/b80.
19
+ FIGUREWIDGET_MARGINS: Mapping[str, int] = MappingProxyType({"l": 16, "t": 32, "r": 16, "b": 16})
20
+
21
+ _DEFAULT_CONFIG: Mapping[str, Any] = MappingProxyType({"responsive": True})
22
+
23
+ # Height of a filling container when nothing constrains it, and its flex basis inside a
24
+ # fill layout; the same 400px shinywidgets gives a FigureWidget.
25
+ _FILL_BASIS = "400px"
26
+
27
+ # Runs right after Plotly.newPlot resolves: hands the graph div to the browser helper
28
+ # (shiny-plotly.js), which tracks its size and purges it when Shiny replaces the output.
29
+ _TRACK_SCRIPT = "window.shinyPlotly && shinyPlotly.track(document.getElementById('{plot_id}'));"
30
+
31
+ Figure = BaseFigure | dict[str, Any]
32
+ """A ``plotly.graph_objects.Figure`` or its JSON dict (``fig.to_dict()``)."""
33
+
34
+
35
+ def fig_to_ui(
36
+ fig: Figure | None,
37
+ div_id: str | None = None,
38
+ *,
39
+ height: str | None = None,
40
+ width: str = "100%",
41
+ figurewidget_margins: bool = False,
42
+ config: Mapping[str, Any] | None = None,
43
+ post_script: str | None = None,
44
+ ) -> TagList | None:
45
+ """
46
+ Turn a plotly figure into a Shiny UI fragment that draws it with ``Plotly.newPlot``.
47
+
48
+ Parameters
49
+ ----------
50
+ fig
51
+ A ``go.Figure`` or its JSON dict. ``None`` renders nothing (returns ``None``).
52
+ div_id
53
+ DOM id of the plotly graph div. A fresh id is generated when omitted.
54
+ height
55
+ CSS height of the plot. ``None`` (the default) fills the parent: inside a fill
56
+ layout (``ui.card(full_screen=True)``, a fillable page, ``output_plotly``) the plot
57
+ grows and shrinks with it from a 400px basis; anywhere else it is 400px tall. A
58
+ value such as ``"300px"`` fixes the height and opts out of filling, exactly like
59
+ ``output_widget(height=...)`` does in shinywidgets.
60
+ width
61
+ CSS width of the plot, ``"100%"`` by default.
62
+ figurewidget_margins
63
+ Fill in margin sides the figure left unset with :data:`FIGUREWIDGET_MARGINS`, the
64
+ values shinywidgets applies to a FigureWidget. Sides the figure sets explicitly win.
65
+ The caller's figure object is never mutated.
66
+ config
67
+ Extra ``Plotly.newPlot`` config, merged over ``{"responsive": True}``.
68
+ post_script
69
+ JavaScript run after the plot is drawn; ``{plot_id}`` is replaced with the graph div
70
+ id. The place to bind plotly events back to Shiny inputs.
71
+ """
72
+ if fig is None:
73
+ return None
74
+ fig_dict = _as_fig_dict(fig)
75
+ if figurewidget_margins:
76
+ _fill_in_margins(fig_dict)
77
+ if div_id is None:
78
+ div_id = "plotly-" + uuid.uuid4().hex
79
+
80
+ fragment = pio.to_html(
81
+ fig_dict,
82
+ validate=False,
83
+ full_html=False,
84
+ include_plotlyjs=False,
85
+ include_mathjax=False,
86
+ div_id=div_id,
87
+ config={**_DEFAULT_CONFIG, **(config or {})},
88
+ post_script=[_TRACK_SCRIPT, post_script] if post_script else [_TRACK_SCRIPT],
89
+ )
90
+ container: Tag = tags.div(
91
+ HTML(fragment),
92
+ class_="shiny-plotly html-fill-item" if height is None else "shiny-plotly",
93
+ style=css(height=height or _FILL_BASIS, width=width),
94
+ )
95
+ return TagList(plotly_js(), shiny_plotly_js(), container)
96
+
97
+
98
+ def _as_fig_dict(fig: Figure) -> dict[str, Any]:
99
+ # Figure.to_dict() does no validation (the figure was validated when built), and a
100
+ # dict is passed through as the caller's JSON; pio.to_html gets validate=False so it
101
+ # never reconstructs a Figure from it.
102
+ if isinstance(fig, BaseFigure):
103
+ return fig.to_dict()
104
+ if isinstance(fig, dict):
105
+ return {**fig, "layout": dict(fig.get("layout") or {})}
106
+ raise TypeError(
107
+ f"fig_to_ui() expects a plotly go.Figure (or its dict), got {type(fig).__name__}"
108
+ )
109
+
110
+
111
+ def _fill_in_margins(fig_dict: dict[str, Any]) -> None:
112
+ layout = fig_dict.setdefault("layout", {})
113
+ layout["margin"] = {**FIGUREWIDGET_MARGINS, **(layout.get("margin") or {})}
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Mapping
4
+ from typing import Any
5
+
6
+ from htmltools import Tag, css
7
+ from shiny import ui as _ui
8
+ from shiny.render import ui as _render_ui
9
+ from shiny.render.renderer import Jsonifiable, ValueFn
10
+ from shiny.session import require_active_session
11
+
12
+ from ._html import Figure, fig_to_ui
13
+
14
+ __all__ = ("output_plotly", "render_plotly")
15
+
16
+
17
+ def output_plotly(id: str, *, width: str | None = None, height: str | None = None) -> Tag:
18
+ """
19
+ Placeholder for a :func:`render_plotly` output. A drop-in for ``output_widget(id)``.
20
+
21
+ It is ``ui.output_ui`` made fill-aware: inside ``ui.card(full_screen=True)`` or a
22
+ fillable page the plot grows and shrinks with its container. Passing ``height`` fixes
23
+ the output's height instead (the plot fills that height), the same rule
24
+ ``output_widget`` follows. ``ui.output_ui(id)`` also works when no fill behaviour is
25
+ wanted.
26
+ """
27
+ return _ui.output_ui(
28
+ id, fill=height is None, fillable=True, style=css(width=width, height=height)
29
+ )
30
+
31
+
32
+ class render_plotly(_render_ui):
33
+ """
34
+ Render a plotly figure as plain HTML drawn with ``Plotly.newPlot``.
35
+
36
+ A drop-in for ``@render_widget`` when the function returns a ``go.Figure`` (or its
37
+ dict). Use bare (``@render_plotly``) or with options::
38
+
39
+ @render_plotly(height="300px", figurewidget_margins=True)
40
+ def sales():
41
+ return px.bar(df, x="month", y="total")
42
+
43
+ Returning ``None`` renders nothing. Works in Core and Express; the decorated function
44
+ may be sync or async.
45
+
46
+ Parameters
47
+ ----------
48
+ height, width
49
+ CSS size of the plot. ``height=None`` (default) fills the output; a value such as
50
+ ``"300px"`` fixes it. See :func:`~shiny_plotly.fig_to_ui`.
51
+ figurewidget_margins
52
+ Fill in unset margin sides with the values shinywidgets applies to a FigureWidget
53
+ (l16/t32/r16/b16), so a migrated app keeps its exact look.
54
+ config
55
+ Extra ``Plotly.newPlot`` config, merged over ``{"responsive": True}``.
56
+ post_script
57
+ JavaScript run after the plot is drawn, with ``{plot_id}`` replaced by the graph
58
+ div's id. The place to forward plotly events to Shiny inputs.
59
+ """
60
+
61
+ def __init__(
62
+ self,
63
+ _fn: ValueFn[Figure | None] | None = None,
64
+ *,
65
+ height: str | None = None,
66
+ width: str = "100%",
67
+ figurewidget_margins: bool = False,
68
+ config: Mapping[str, Any] | None = None,
69
+ post_script: str | None = None,
70
+ ) -> None:
71
+ self.height = height
72
+ self.width = width
73
+ self.figurewidget_margins = figurewidget_margins
74
+ self.config = config
75
+ self.post_script = post_script
76
+ # Registers _fn (sets output_id from its name) when used as a bare decorator.
77
+ super().__init__(_fn) # type: ignore[arg-type]
78
+
79
+ def auto_output_ui(self) -> Tag:
80
+ return output_plotly(self.output_id)
81
+
82
+ async def transform(self, value: Figure) -> Jsonifiable: # type: ignore[override]
83
+ session = require_active_session(None)
84
+ fragment = fig_to_ui(
85
+ value,
86
+ div_id=f"{session.ns(self.output_id)}-plotly",
87
+ height=self.height,
88
+ width=self.width,
89
+ figurewidget_margins=self.figurewidget_margins,
90
+ config=self.config,
91
+ post_script=self.post_script,
92
+ )
93
+ return await super().transform(fragment)
shiny_plotly/py.typed ADDED
File without changes
@@ -0,0 +1,47 @@
1
+ // Keeps every shiny-plotly graph sized to its container, and releases a graph once Shiny
2
+ // has replaced the output that held it.
3
+ //
4
+ // Plotly re-measures a responsive graph only on window resize, and it registers one window
5
+ // listener per graph div. So a graph whose card changes size without a window resize (a
6
+ // sibling output rendering, a sidebar toggle) stays at its old size, and a graph Shiny has
7
+ // re-rendered is kept alive by that listener forever. One ResizeObserver covers both: a
8
+ // size change re-lays the graph out, and the notification for a detached element (size 0x0,
9
+ // no longer in the document) purges it, which removes the listener and plotly's state.
10
+ (function () {
11
+ "use strict";
12
+
13
+ var observer = null;
14
+
15
+ function differs(actual, laidOut) {
16
+ // Same tolerance plotly's own autosize applies before it redraws.
17
+ return Math.abs(actual - laidOut) > 1;
18
+ }
19
+
20
+ function onResize(entries) {
21
+ for (var i = 0; i < entries.length; i++) {
22
+ var gd = entries[i].target;
23
+ if (!gd.isConnected) {
24
+ observer.unobserve(gd);
25
+ if (window.Plotly) window.Plotly.purge(gd);
26
+ continue;
27
+ }
28
+ var rect = entries[i].contentRect;
29
+ var layout = gd._fullLayout;
30
+ if (!layout || (rect.width === 0 && rect.height === 0)) continue;
31
+ if (differs(rect.width, layout.width) || differs(rect.height, layout.height)) {
32
+ window.Plotly.Plots.resize(gd);
33
+ }
34
+ }
35
+ }
36
+
37
+ window.shinyPlotly = {
38
+ // Called from the fragment right after Plotly.newPlot resolves, with the graph div.
39
+ track: function (gd) {
40
+ if (!gd) return;
41
+ if (observer === null && typeof ResizeObserver === "function") {
42
+ observer = new ResizeObserver(onResize);
43
+ }
44
+ if (observer !== null) observer.observe(gd);
45
+ }
46
+ };
47
+ })();
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.5
2
+ Name: shiny-plotly
3
+ Version: 0.1.0
4
+ Summary: Render plotly figures in Shiny for Python as plain HTML, without the shinywidgets layer.
5
+ Project-URL: Homepage, https://github.com/rvben/shiny-plotly
6
+ Project-URL: Repository, https://github.com/rvben/shiny-plotly
7
+ Project-URL: Changelog, https://github.com/rvben/shiny-plotly/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/rvben/shiny-plotly/issues
9
+ Author-email: "Ruben J. Jongejan" <ruben.jongejan@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: dashboard,html,plotly,render,shiny
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Scientific/Engineering :: Visualization
22
+ Classifier: Typing :: Typed
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: htmltools>=0.5
25
+ Requires-Dist: plotly>=5.0
26
+ Requires-Dist: shiny>=1.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # shiny-plotly
30
+
31
+ Render [plotly](https://plotly.com/python/) figures in [Shiny for Python](https://shiny.posit.co/py/) as plain HTML, without the shinywidgets layer.
32
+
33
+ *An independent project, not affiliated with or endorsed by Posit or Plotly.*
34
+
35
+ [![PyPI](https://img.shields.io/pypi/v/shiny-plotly)](https://pypi.org/project/shiny-plotly/)
36
+ [![CI](https://github.com/rvben/shiny-plotly/actions/workflows/ci.yml/badge.svg)](https://github.com/rvben/shiny-plotly/actions/workflows/ci.yml)
37
+
38
+ ```python
39
+ from shiny_plotly import output_plotly, render_plotly
40
+
41
+ # UI
42
+ output_plotly("sales")
43
+
44
+
45
+ # server
46
+ @render_plotly
47
+ def sales():
48
+ return go.Figure(go.Bar(x=months, y=totals))
49
+ ```
50
+
51
+ That is the whole API surface for the common case. The figure is serialized with plotly's own `to_html`, sent to the browser as a UI fragment, and drawn with `Plotly.newPlot`. No ipywidgets, no kernel comm, no anywidget. Every render replaces the figure, which is how most dashboards already use `@render_widget`.
52
+
53
+ ## Why
54
+
55
+ `shinywidgets` renders a plotly figure by wrapping it in a `FigureWidget` and shipping it through the ipywidgets comm protocol. That machinery earns its keep when the app mutates a figure in place (`fig.data[0].y = ...`) and wants the browser to patch it. Most Shiny apps do not do that; they rebuild the figure inside a reactive function and let Shiny re-render the output. For those apps the widget layer is overhead:
56
+
57
+ - extra dependencies (`ipywidgets`, `anywidget`, `shinywidgets`) and their JavaScript bundles on every page;
58
+ - a second rendering path next to Shiny's own, with its own quirks around sizing and full screen;
59
+ - figures held as widget state on the server for the life of the session.
60
+
61
+ `shiny-plotly` renders the figure the way plotly itself does, as HTML plus `Plotly.newPlot`, and uses Shiny's `render.ui` for delivery. The plotly.js bundle is served straight from the installed `plotly` wheel, keyed by its version, so nothing is copied or vendored.
62
+
63
+ ## Install
64
+
65
+ ```sh
66
+ uv add shiny-plotly
67
+ # or
68
+ pip install shiny-plotly
69
+ ```
70
+
71
+ Requires Python 3.10+, `shiny>=1.0`, `plotly>=5.0`.
72
+
73
+ ## Use
74
+
75
+ ### Core
76
+
77
+ ```python
78
+ import random
79
+ from itertools import accumulate
80
+
81
+ import plotly.graph_objects as go
82
+ from shiny import App, ui
83
+
84
+ from shiny_plotly import output_plotly, plotly_js, render_plotly
85
+
86
+ app_ui = ui.page_fillable(
87
+ ui.input_slider("n", "Points", 10, 500, 100),
88
+ ui.card(
89
+ ui.card_header("Fills the card; try full screen"),
90
+ output_plotly("walk"),
91
+ full_screen=True,
92
+ ),
93
+ plotly_js(), # optional: load plotly.js with the page instead of with the first figure
94
+ )
95
+
96
+
97
+ def server(input, output, session):
98
+ @render_plotly
99
+ def walk():
100
+ rng = random.Random(input.n())
101
+ y = list(accumulate(rng.gauss(0, 1) for _ in range(input.n())))
102
+ return go.Figure(go.Scatter(y=y, mode="lines"))
103
+
104
+
105
+ app = App(app_ui, server)
106
+ ```
107
+
108
+ Anything that is a `plotly.graph_objects.Figure` works, including what `plotly.express` builds (install `plotly[express]` for that).
109
+
110
+ ### Express
111
+
112
+ ```python
113
+ import random
114
+ from itertools import accumulate
115
+
116
+ import plotly.graph_objects as go
117
+ from shiny.express import input, ui
118
+
119
+ from shiny_plotly import render_plotly
120
+
121
+ ui.page_opts(fillable=True)
122
+
123
+ with ui.sidebar():
124
+ ui.input_slider("n", "Points", 10, 500, 100)
125
+
126
+ with ui.card(full_screen=True):
127
+
128
+ @render_plotly
129
+ def walk():
130
+ rng = random.Random(input.n())
131
+ y = list(accumulate(rng.gauss(0, 1) for _ in range(input.n())))
132
+ return go.Figure(go.Scatter(y=y, mode="lines"))
133
+ ```
134
+
135
+ The decorator creates its own output placeholder in Express, just like `@render_widget` does.
136
+
137
+ ### Options
138
+
139
+ ```python
140
+ @render_plotly(
141
+ height="300px", # fixed height; default None fills the container
142
+ width="100%",
143
+ figurewidget_margins=True, # the l16/t32/r16/b16 margins shinywidgets applies
144
+ config={"displaylogo": False},
145
+ post_script=CLICK_TO_INPUT, # JavaScript run after the plot is drawn
146
+ )
147
+ def sales(): ...
148
+ ```
149
+
150
+ `None` from the render function renders nothing. The function may be sync or async. It may also return `fig.to_dict()` instead of a `Figure`.
151
+
152
+ ### Migrating from shinywidgets
153
+
154
+ | shinywidgets | shiny-plotly |
155
+ | --- | --- |
156
+ | `from shinywidgets import output_widget, render_widget` | `from shiny_plotly import output_plotly, render_plotly` |
157
+ | `output_widget("id")` | `output_plotly("id")` |
158
+ | `output_widget("id", height="300px")` | `output_plotly("id", height="300px")` |
159
+ | `@render_widget` | `@render_plotly` |
160
+ | (FigureWidget margins, applied implicitly) | `@render_plotly(figurewidget_margins=True)` |
161
+
162
+ Two things change on purpose:
163
+
164
+ - **Margins.** shinywidgets sets tight margins (`l=16, t=32, r=16, b=16`) on every FigureWidget; plotly's own defaults are `80/100/80/80`. `shiny-plotly` uses plotly's defaults unless you pass `figurewidget_margins=True`, which fills in only the sides your figure leaves unset. Set margins explicitly on the figure if you want something else.
165
+ - **In-place mutation.** A `FigureWidget` you keep on the server and mutate (`fig.data[0].y = ...`, `fig.add_trace(...)` after render) is exactly what shinywidgets is for. `shiny-plotly` has no channel for that; return a new figure from the render function and let Shiny re-render. If your app depends on in-place widget updates, stay on shinywidgets for those outputs. Both packages can coexist in one app.
166
+
167
+ ### Sizing
168
+
169
+ The rules mirror `output_widget`:
170
+
171
+ - `height=None` (default): the plot fills its container. Inside `ui.card(full_screen=True)`, a fillable page or a sidebar layout it grows and shrinks with the card, from a 400px basis. Outside a fill layout it is 400px tall.
172
+ - `height="300px"` (on the decorator or on `output_plotly`): the plot is exactly that tall and opts out of filling.
173
+
174
+ Plotly alone re-measures a graph only on window resize. `shiny-plotly` ships a small helper script (`shiny-plotly.js`, loaded with every figure) that observes each graph's container with a `ResizeObserver`, so a card that changes size without a window resize, for example when a sibling output renders below it, or when a sidebar collapses, re-lays the graph out. The same helper purges a graph once Shiny has replaced the output holding it, which releases the window listener and layout state plotly would otherwise keep for every render.
175
+
176
+ ### Events back to Shiny
177
+
178
+ `post_script` runs after `Plotly.newPlot` resolves; `{plot_id}` is replaced with the graph div's id.
179
+
180
+ ```python
181
+ CLICK_TO_INPUT = """
182
+ document.getElementById('{plot_id}').on('plotly_click', function (ev) {
183
+ var p = ev.points[0];
184
+ Shiny.setInputValue('clicked', {x: p.x, y: p.y}, {priority: 'event'});
185
+ });
186
+ """
187
+
188
+
189
+ @render_plotly(post_script=CLICK_TO_INPUT)
190
+ def scatter(): ...
191
+
192
+
193
+ @render.text
194
+ def click_info():
195
+ if not input.clicked.is_set():
196
+ return "Click a point."
197
+ pt = input.clicked()
198
+ return f"x={pt['x']}, y={pt['y']}"
199
+ ```
200
+
201
+ `input.clicked()` raises a silent exception while the input has never been set, so check `is_set()` first when the output should show something before the first click.
202
+
203
+ ### Lower level
204
+
205
+ - `fig_to_ui(fig, div_id=None, *, height, width, figurewidget_margins, config, post_script)` returns the `TagList` a render produces: the plotly.js dependency, the helper dependency and the `<div class="shiny-plotly">` holding the figure. Use it from a plain `@render.ui` that composes several things, or from any htmltools context.
206
+ - `plotly_js()` is the `HTMLDependency` for plotly.js, served from the installed `plotly` wheel at `/lib/plotly-<version>/plotly.min.js`. Every figure carries it, so it is optional; add it to the page UI to load the bundle up front instead of with the first figure.
207
+ - `shiny_plotly_js()` is the helper's dependency. Every figure carries it too.
208
+ - `FIGUREWIDGET_MARGINS` is the `{"l": 16, "t": 32, "r": 16, "b": 16}` mapping.
209
+
210
+ ## Examples
211
+
212
+ ```sh
213
+ uv run --with shiny-plotly shiny run examples/core_app.py
214
+ uv run --with shiny-plotly shiny run examples/express_app.py
215
+ ```
216
+
217
+ ## Development
218
+
219
+ ```sh
220
+ make sync # uv sync --all-groups
221
+ make browsers # playwright install chromium, once
222
+ make check # lint, typecheck, unit + e2e tests, browser tests, wheel check
223
+ ```
224
+
225
+ `make test` runs the unit tests and the in-process Shiny end-to-end tests over a real websocket. `make test-browser` drives the package in headless Chromium: fill sizing, resize without a window event, purge on re-render, full screen, `post_script` click wiring and on-demand loading of plotly.js. `make check-wheel` installs the built wheel into a throwaway venv and runs the suite against it, so the published artifact is what was tested.
226
+
227
+ ## License
228
+
229
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,10 @@
1
+ shiny_plotly/__init__.py,sha256=FKfM23h6W68_CB6vFaiQtaHlf-yTbSFTp705uaKKaZw,402
2
+ shiny_plotly/_deps.py,sha256=-Pay-nYalROIeX9lyu9HZhCwVfchRrIFP51DQk3UfQ8,1789
3
+ shiny_plotly/_html.py,sha256=vjXxntDCP4J7tz2RHvMwvOxU9Fd9FNW7_4sXMCMA3T8,4483
4
+ shiny_plotly/_render.py,sha256=IlPk4ngFAb3GvccEsDViLTRWR2y-TtbVetcVsFojt2w,3446
5
+ shiny_plotly/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ shiny_plotly/www/shiny-plotly.js,sha256=R6HsW8T8qjcBeqL_DR5_yep_kRhxk0_-9Iih1__ZV_I,1794
7
+ shiny_plotly-0.1.0.dist-info/METADATA,sha256=kja8lvqOHWrnDcaoav3HfR6cfHkp3D7OSDlnmUF5Pm4,9839
8
+ shiny_plotly-0.1.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
9
+ shiny_plotly-0.1.0.dist-info/licenses/LICENSE,sha256=5pr_hilUw8EqNYDfW-9iOtxur_eDNkxklljAsSKi9qo,1074
10
+ shiny_plotly-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruben J. Jongejan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.