Skip to content

Commit c1d1173

Browse files
author
deeleeramone
committed
multi widget example and to_html guide
1 parent 455323a commit c1d1173

5 files changed

Lines changed: 706 additions & 58 deletions

File tree

pywry/docs/docs/getting-started/why-pywry.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ Native windows open in under a second. There's no server to spin up, no browser
7070
PyWry isn't just for prototyping and single-user applications:
7171

7272
- **Deploy Mode** with Redis backend for horizontal scaling and RBAC
73+
- **0Auth2"** authentication system for both native and deploy modes
7374
- **Token authentication** and CSRF protection out of the box
7475
- **CSP headers** and security presets for production environments
7576
- **TOML-based configuration** with layered precedence (defaults → project → user → env vars)
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
# Multi-Widget Pages
2+
3+
`show_plotly()` and `show_dataframe()` each render a single widget. To combine multiple widgets — charts, grids, forms, tickers — in one window, use `build_html()` to generate each piece and compose them with `Div`.
4+
5+
## The Pattern
6+
7+
1. **Generate HTML snippets**`build_html()` on any component returns a self-contained HTML string.
8+
2. **Compose with `Div`** — Nest `Div` objects to build your page tree. Style with CSS using `--pywry-*` theme variables.
9+
3. **Show** — Pass combined HTML as `HtmlContent` to `app.show()` with `include_plotly` / `include_aggrid` flags as needed.
10+
11+
Every toolbar component (`Button`, `Select`, `Toggle`, `TextInput`, `Checkbox`, `RadioGroup`, `TabGroup`, `SliderInput`, `RangeInput`, `NumberInput`, `DateInput`, `SearchInput`, `SecretInput`, `TextArea`, `MultiSelect`, `Marquee`, `TickerItem`), plus `Toolbar`, `Modal`, and `Div` all expose `build_html()`. See the [Toolbar System](toolbars.md) and [Modals](modals.md) guides for their APIs.
12+
13+
---
14+
15+
## Widget Snippets
16+
17+
### Plotly
18+
19+
```python
20+
import json
21+
from pywry.templates import build_plotly_init_script
22+
23+
fig_dict = json.loads(fig.to_json()) # must be a plain dict, not a Figure
24+
chart_html = build_plotly_init_script(figure=fig_dict, chart_id="my-chart")
25+
```
26+
27+
Requires `include_plotly=True` on `app.show()`. Target later with `widget.emit("plotly:update-figure", {"figure": new_dict, "chartId": "my-chart"})`.
28+
29+
### AG Grid
30+
31+
```python
32+
from pywry.grid import build_grid_config, build_grid_html
33+
34+
config = build_grid_config(df, grid_id="my-grid", row_selection=True)
35+
grid_html = build_grid_html(config)
36+
```
37+
38+
Requires `include_aggrid=True` on `app.show()`. Target later with `widget.emit("grid:update-data", {"data": rows, "gridId": "my-grid"})`.
39+
40+
Use unique `chart_id` / `grid_id` values when placing multiple charts or grids on the same page.
41+
42+
---
43+
44+
## Composing with `Div`
45+
46+
`Div` is the layout primitive. Use `content` for raw HTML (widget snippets, headings, text) and `children` for nested component objects. Both render in order: `content` first, then `children`.
47+
48+
```python
49+
from pywry import Div, Button, Toggle
50+
51+
dashboard = Div(
52+
class_name="dashboard",
53+
children=[
54+
Div(class_name="kpi-row", children=[
55+
Div(class_name="kpi-card", content='<span class="label">Revenue</span><span class="value">$318K</span>'),
56+
Div(class_name="kpi-card", content='<span class="label">Users</span><span class="value">1,247</span>'),
57+
]),
58+
Div(class_name="content-row", children=[
59+
Div(class_name="chart-panel", content=chart_html),
60+
Div(class_name="grid-panel", content=grid_html),
61+
]),
62+
Div(class_name="controls", children=[
63+
Toggle(label="Live:", event="app:live", value=True),
64+
Button(label="Export", event="app:export", variant="secondary"),
65+
]),
66+
],
67+
)
68+
69+
page_html = dashboard.build_html()
70+
```
71+
72+
- `class_name` is added alongside the automatic `pywry-div` class — target it in CSS
73+
- Nested `Div`s pass parent context via `data-parent-id` automatically
74+
75+
### Scripts
76+
77+
`Div` and `Toolbar` accept a `script` field (inline JS or file path). `build_html()` resolves it and emits a `<script>` tag inside the `<div>`:
78+
79+
```python
80+
panel = Div(content="<p>Hello</p>", script="console.log('loaded');")
81+
# → <div class="pywry-div" ...><p>Hello</p><script>console.log('loaded');</script></div>
82+
83+
panel = Div(content="<p>Chart</p>", script="static/chart_init.js") # reads file
84+
```
85+
86+
`collect_scripts()` is also available if you need raw script strings without HTML wrapping.
87+
88+
---
89+
90+
## Showing the Page
91+
92+
```python
93+
from pywry import PyWry, HtmlContent
94+
95+
app = PyWry(title="Dashboard", width=1200, height=780)
96+
97+
content = HtmlContent(html=dashboard.build_html(), inline_css=css)
98+
99+
widget = app.show(
100+
content,
101+
include_plotly=True,
102+
include_aggrid=True,
103+
toolbars=[toolbar], # optional positioned toolbar bars
104+
modals=[modal], # optional modals
105+
callbacks={
106+
"plotly:click": on_chart_click,
107+
"grid:row-selected": on_row_selected,
108+
"app:export": on_export,
109+
},
110+
)
111+
```
112+
113+
- Components embedded directly in your HTML emit events via `window.pywry.emit()` without needing `toolbars=`
114+
- Toolbars passed via `toolbars=` are auto-positioned (top, bottom, left, right)
115+
- Modals passed via `modals=` are auto-injected with open/close wiring
116+
117+
### Sizing Tips
118+
119+
- Plotly: set `width: 100% !important; height: 100% !important` on `.pywry-plotly` and give its parent a flex layout
120+
- AG Grid: needs a parent with defined height — `flex: 1` inside a flex column works
121+
- Use `min-height: 0` on flex children that need to shrink below content size
122+
123+
---
124+
125+
## Cross-Widget Events
126+
127+
With everything in one page, wire interactions through callbacks — no JavaScript needed:
128+
129+
```python
130+
# Grid selection → update chart
131+
def on_row_selected(data, _event_type, _label):
132+
rows = data.get("rows", [])
133+
widget.emit("plotly:update-figure", {"figure": filtered_fig, "chartId": "my-chart"})
134+
135+
# Chart click → update detail panel
136+
def on_chart_click(data, _event_type, _label):
137+
point = data.get("points", [{}])[0]
138+
widget.emit("pywry:set-content", {"id": "detail-panel", "html": f"<b>{point['x']}</b>"})
139+
140+
# Export CSV
141+
def on_export(_data, _event_type, _label):
142+
widget.emit("pywry:download", {"content": df.to_csv(index=False), "filename": "data.csv", "mimeType": "text/csv"})
143+
```
144+
145+
See the [Event System guide](events.md) for the full list of system events (`pywry:set-content`, `pywry:download`, `plotly:update-figure`, `grid:update-data`, etc.).
146+
147+
---
148+
149+
## Complete Example
150+
151+
See [`examples/pywry_demo_multi_widget.py`](https://github.com/deeleeramone/PyWry/blob/main/pywry/examples/pywry_demo_multi_widget.py) for a full working dashboard with KPI cards, Plotly chart, AG Grid, toolbar, cross-widget filtering, and CSV export.
152+
153+
---
154+
155+
## Related Guides
156+
157+
- [Toolbar System](toolbars.md) — all toolbar component types and their APIs
158+
- [Modals](modals.md) — modal overlay components
159+
- [Event System](events.md) — event registration and dispatch
160+
- [Theming & CSS](theming.md)`--pywry-*` variables and theme switching
161+
- [HtmlContent](html-content.md) — CSS files, script files, inline CSS, JSON data
162+
- [Content Assembly](content-assembly.md) — what PyWry injects into the document

pywry/docs/mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ nav:
177177
- Integrations:
178178
- Plotly Charts: guides/plotly.md
179179
- AgGrid Tables: guides/aggrid.md
180+
- Multi-Widget Pages: guides/multi-widget.md
180181
- Hosting:
181182
- Browser Mode: guides/browser-mode.md
182183
- Deploy Mode: guides/deploy-mode.md

0 commit comments

Comments
 (0)