lattice-grid-dash 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.
- lattice_grid_dash-0.1.0/.gitignore +31 -0
- lattice_grid_dash-0.1.0/PKG-INFO +148 -0
- lattice_grid_dash-0.1.0/README.md +131 -0
- lattice_grid_dash-0.1.0/build.mjs +38 -0
- lattice_grid_dash-0.1.0/examples/app.py +53 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/LatticeGrid.py +95 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/__init__.py +76 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/_imports_.py +5 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/_serialize_bridge.py +79 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/lattice-grid.min.css +1 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/lattice_grid_dash.min.js +15937 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/metadata.json +1 -0
- lattice_grid_dash-0.1.0/lattice_grid_dash/package-info.json +20 -0
- lattice_grid_dash-0.1.0/package-info.json +7 -0
- lattice_grid_dash-0.1.0/package.json +20 -0
- lattice_grid_dash-0.1.0/pyproject.toml +51 -0
- lattice_grid_dash-0.1.0/src/lib/bundle.js +13 -0
- lattice_grid_dash-0.1.0/src/lib/components/LatticeGrid.react.js +190 -0
- lattice_grid_dash-0.1.0/src/lib/index.js +4 -0
- lattice_grid_dash-0.1.0/src/lib/react-shim.js +15 -0
- lattice_grid_dash-0.1.0/tests/test_bridge.py +45 -0
- lattice_grid_dash-0.1.0/tests/test_smoke_browser.py +237 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python build / cache artifacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
develop-eggs/
|
|
8
|
+
.pytest_cache/
|
|
9
|
+
.mypy_cache/
|
|
10
|
+
.ruff_cache/
|
|
11
|
+
|
|
12
|
+
# Local build outputs (wheels/sdists are built by CI on tag)
|
|
13
|
+
/dist/
|
|
14
|
+
/dist-1.41/
|
|
15
|
+
packages/*/dist/
|
|
16
|
+
|
|
17
|
+
# Virtual environments
|
|
18
|
+
.venv/
|
|
19
|
+
venv/
|
|
20
|
+
env/
|
|
21
|
+
|
|
22
|
+
# Node (the Dash component's build toolchain; its BUILT bundle
|
|
23
|
+
# lattice_grid_dash/lattice_grid_dash.min.js is committed on purpose)
|
|
24
|
+
node_modules/
|
|
25
|
+
packages/*/node_modules/
|
|
26
|
+
|
|
27
|
+
# Editor / OS
|
|
28
|
+
.DS_Store
|
|
29
|
+
*.swp
|
|
30
|
+
.idea/
|
|
31
|
+
.vscode/
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lattice-grid-dash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Edit a pandas DataFrame as a Lattice Grid inside a Dash app, with edits and selection surfaced back to Python callbacks.
|
|
5
|
+
Project-URL: Homepage, https://latticegrid.dev
|
|
6
|
+
License: Proprietary
|
|
7
|
+
Keywords: component,dash,dataframe,datagrid,lattice-grid,pandas,plotly
|
|
8
|
+
Requires-Python: >=3.9
|
|
9
|
+
Requires-Dist: dash>=2.9
|
|
10
|
+
Requires-Dist: lattice-grid-pandas==0.1.0
|
|
11
|
+
Requires-Dist: pandas>=1.5
|
|
12
|
+
Provides-Extra: dev
|
|
13
|
+
Requires-Dist: playwright>=1.40; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
15
|
+
Requires-Dist: requests; extra == 'dev'
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# lattice-grid-dash
|
|
19
|
+
|
|
20
|
+
A [Dash](https://dash.plotly.com/) component that renders an editable
|
|
21
|
+
[Lattice Grid](https://latticegrid.dev) over a pandas `DataFrame`, with cell edits
|
|
22
|
+
and selection surfaced back to Python `@callback`s.
|
|
23
|
+
|
|
24
|
+
Phase **C2** of the Lattice Grid Python integration (card BACKLOG-0000972,
|
|
25
|
+
release 1.41). Built on the same shared serialization layer as the Jupyter widget
|
|
26
|
+
(phase C1): [`lattice-grid-pandas`](../lattice-grid-pandas).
|
|
27
|
+
|
|
28
|
+
## How it works
|
|
29
|
+
|
|
30
|
+
The component does **not** re-implement a React binding for the grid. It consumes
|
|
31
|
+
the grid's **shipped React adapter** (`@toclocoinc/lattice-grid/modules/react`,
|
|
32
|
+
grid v1.40.0), which exposes `createLatticeGrid({ React, createGrid })`. We feed
|
|
33
|
+
it Dash's own React (aliased at bundle time to the global the dash-renderer
|
|
34
|
+
serves) so there is a single React instance on the page. This is the lowest-risk
|
|
35
|
+
path identified in the spike: the grid team owns and versions the adapter.
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
DataFrame ──dataframe_to_data()──▶ data prop (columnar, from lattice-grid-pandas)
|
|
39
|
+
│
|
|
40
|
+
LatticeGrid (React adapter → createGrid)
|
|
41
|
+
│ manual edit
|
|
42
|
+
cellChanged prop ◀──setProps── onCellChanged (adapter callback)
|
|
43
|
+
│
|
|
44
|
+
Python @callback(Input("grid", "cellChanged"))
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Install
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install lattice-grid-dash
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
(Editable/dev: `pip install -e packages/lattice-grid-dash`. The built JS bundle is
|
|
54
|
+
committed under `lattice_grid_dash/`; rebuild it with `npm install && npm run build`.)
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
import pandas as pd
|
|
60
|
+
from dash import Dash, Input, Output, callback, html
|
|
61
|
+
import lattice_grid_dash
|
|
62
|
+
from lattice_grid_dash import dataframe_to_data, apply_cell_edit
|
|
63
|
+
|
|
64
|
+
df = pd.DataFrame({"name": ["Ada", "Grace"], "score": [91, 88], "active": [True, False]})
|
|
65
|
+
|
|
66
|
+
app = Dash(__name__)
|
|
67
|
+
app.layout = html.Div([
|
|
68
|
+
lattice_grid_dash.LatticeGrid(id="grid", data=dataframe_to_data(df)),
|
|
69
|
+
html.Pre(id="out"),
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
@callback(Output("out", "children"), Input("grid", "cellChanged"))
|
|
73
|
+
def on_edit(edit):
|
|
74
|
+
if not edit:
|
|
75
|
+
return "no edits yet"
|
|
76
|
+
apply_cell_edit(df, edit) # keep the server-side DataFrame in sync, typed
|
|
77
|
+
return f"{edit['colId']} @ row {edit['key']} = {edit['value']}"
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
app.run(debug=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
See [`examples/app.py`](examples/app.py).
|
|
84
|
+
|
|
85
|
+
## Props
|
|
86
|
+
|
|
87
|
+
| Prop | Direction | Description |
|
|
88
|
+
| --- | --- | --- |
|
|
89
|
+
| `data` | in | DataFrame-derived columnar payload from `dataframe_to_data(df)`: `{columns, rowKey, columnar}`. Rows are reconstructed and virtualized in the browser. |
|
|
90
|
+
| `columns` | in | Optional explicit column defs (overrides `data.columns`). |
|
|
91
|
+
| `options` | in | Passthrough to `createGrid` (e.g. `{"edit": True, "rowHeight": 32}`). |
|
|
92
|
+
| `licence` | in | Lattice Grid licence key. Empty on localhost → free/unwatermarked. |
|
|
93
|
+
| `cellChanged` | **out** | Last manual edit `{key, colId, value, old, ts}`. Drives Python callbacks. |
|
|
94
|
+
| `selectedKeys` | in/out | Selected row keys; set by the grid, settable from Python. |
|
|
95
|
+
| `licenceState` | **out** | Grid's resolved licence state (e.g. `"localhost"`). |
|
|
96
|
+
| `style`, `className` | in | Host element styling. |
|
|
97
|
+
|
|
98
|
+
## Licence plumbing
|
|
99
|
+
|
|
100
|
+
The `licence` prop is a single opaque string handed straight to
|
|
101
|
+
`createGrid({ licence })`; the grid resolves it **client-side** (localhost origins
|
|
102
|
+
run free, unwatermarked, with no key). The resolved state is surfaced back on the
|
|
103
|
+
`licenceState` prop. This is the same contract as the Jupyter widget — the shared
|
|
104
|
+
rules live in `lattice_grid_pandas._grid`.
|
|
105
|
+
|
|
106
|
+
## Grid delivery: vendored (default) vs CDN
|
|
107
|
+
|
|
108
|
+
The grid JavaScript can reach the browser two ways. **This package implements the
|
|
109
|
+
vendored path**; the CDN path is documented here for operators who prefer it.
|
|
110
|
+
|
|
111
|
+
### Vendored (implemented)
|
|
112
|
+
|
|
113
|
+
`npm run build` bundles the grid core + React adapter into
|
|
114
|
+
`lattice_grid_dash/lattice_grid_dash.min.js` (~2.7 MB) via `esbuild`, and the grid
|
|
115
|
+
CSS is vendored as `lattice_grid_dash/lattice-grid.min.css`. Both are registered
|
|
116
|
+
as Dash assets (`_js_dist` / `_css_dist`), so the component works **fully offline**
|
|
117
|
+
— no network at render time. This mirrors the C1 widget's `offline=True` default
|
|
118
|
+
and is what the browser smoke test exercises.
|
|
119
|
+
|
|
120
|
+
Rebuild:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
cd packages/lattice-grid-dash
|
|
124
|
+
npm install
|
|
125
|
+
npm run build # build:js (esbuild) + build:py (dash-generate-components)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### CDN (documented alternative)
|
|
129
|
+
|
|
130
|
+
To ship a thin component bundle that loads the grid from jsDelivr at render time
|
|
131
|
+
(smaller wheel, needs network + a relaxed CSP):
|
|
132
|
+
|
|
133
|
+
1. In `build.mjs`, mark the grid packages external:
|
|
134
|
+
`external: ['react-dom', '@toclocoinc/lattice-grid', '@toclocoinc/lattice-grid/modules/react']`.
|
|
135
|
+
2. Before the bundle loads, inject the grid from the CDN, e.g.
|
|
136
|
+
`https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.40.0/lattice-grid.esm.min.js`
|
|
137
|
+
and `.../modules/react.esm.min.js` (URLs available from
|
|
138
|
+
`lattice_grid_pandas.cdn_urls()`), and add the stylesheet
|
|
139
|
+
`.../lattice-grid.min.css`.
|
|
140
|
+
3. Drop the vendored files from `_js_dist` / `_css_dist`.
|
|
141
|
+
|
|
142
|
+
The two are mutually exclusive; pick one per build.
|
|
143
|
+
|
|
144
|
+
## Development / build toolchain
|
|
145
|
+
|
|
146
|
+
- `npm run build:js` — `esbuild` bundles `src/lib/bundle.js` → `lattice_grid_dash/lattice_grid_dash.min.js`. `react` is aliased to Dash's global React (`src/lib/react-shim.js`); react/react-dom are never bundled.
|
|
147
|
+
- `npm run build:py` — `dash-generate-components` (react-docgen v5) reads `src/lib/components/LatticeGrid.react.js` and generates `LatticeGrid.py`, `_imports_.py`, and `metadata.json`.
|
|
148
|
+
- `pytest tests/` — unit tests for the data bridge, plus a real-browser Playwright smoke test (system Chrome via `channel='chrome'`).
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# lattice-grid-dash
|
|
2
|
+
|
|
3
|
+
A [Dash](https://dash.plotly.com/) component that renders an editable
|
|
4
|
+
[Lattice Grid](https://latticegrid.dev) over a pandas `DataFrame`, with cell edits
|
|
5
|
+
and selection surfaced back to Python `@callback`s.
|
|
6
|
+
|
|
7
|
+
Phase **C2** of the Lattice Grid Python integration (card BACKLOG-0000972,
|
|
8
|
+
release 1.41). Built on the same shared serialization layer as the Jupyter widget
|
|
9
|
+
(phase C1): [`lattice-grid-pandas`](../lattice-grid-pandas).
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
The component does **not** re-implement a React binding for the grid. It consumes
|
|
14
|
+
the grid's **shipped React adapter** (`@toclocoinc/lattice-grid/modules/react`,
|
|
15
|
+
grid v1.40.0), which exposes `createLatticeGrid({ React, createGrid })`. We feed
|
|
16
|
+
it Dash's own React (aliased at bundle time to the global the dash-renderer
|
|
17
|
+
serves) so there is a single React instance on the page. This is the lowest-risk
|
|
18
|
+
path identified in the spike: the grid team owns and versions the adapter.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
DataFrame ──dataframe_to_data()──▶ data prop (columnar, from lattice-grid-pandas)
|
|
22
|
+
│
|
|
23
|
+
LatticeGrid (React adapter → createGrid)
|
|
24
|
+
│ manual edit
|
|
25
|
+
cellChanged prop ◀──setProps── onCellChanged (adapter callback)
|
|
26
|
+
│
|
|
27
|
+
Python @callback(Input("grid", "cellChanged"))
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Install
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install lattice-grid-dash
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
(Editable/dev: `pip install -e packages/lattice-grid-dash`. The built JS bundle is
|
|
37
|
+
committed under `lattice_grid_dash/`; rebuild it with `npm install && npm run build`.)
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import pandas as pd
|
|
43
|
+
from dash import Dash, Input, Output, callback, html
|
|
44
|
+
import lattice_grid_dash
|
|
45
|
+
from lattice_grid_dash import dataframe_to_data, apply_cell_edit
|
|
46
|
+
|
|
47
|
+
df = pd.DataFrame({"name": ["Ada", "Grace"], "score": [91, 88], "active": [True, False]})
|
|
48
|
+
|
|
49
|
+
app = Dash(__name__)
|
|
50
|
+
app.layout = html.Div([
|
|
51
|
+
lattice_grid_dash.LatticeGrid(id="grid", data=dataframe_to_data(df)),
|
|
52
|
+
html.Pre(id="out"),
|
|
53
|
+
])
|
|
54
|
+
|
|
55
|
+
@callback(Output("out", "children"), Input("grid", "cellChanged"))
|
|
56
|
+
def on_edit(edit):
|
|
57
|
+
if not edit:
|
|
58
|
+
return "no edits yet"
|
|
59
|
+
apply_cell_edit(df, edit) # keep the server-side DataFrame in sync, typed
|
|
60
|
+
return f"{edit['colId']} @ row {edit['key']} = {edit['value']}"
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
app.run(debug=True)
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
See [`examples/app.py`](examples/app.py).
|
|
67
|
+
|
|
68
|
+
## Props
|
|
69
|
+
|
|
70
|
+
| Prop | Direction | Description |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `data` | in | DataFrame-derived columnar payload from `dataframe_to_data(df)`: `{columns, rowKey, columnar}`. Rows are reconstructed and virtualized in the browser. |
|
|
73
|
+
| `columns` | in | Optional explicit column defs (overrides `data.columns`). |
|
|
74
|
+
| `options` | in | Passthrough to `createGrid` (e.g. `{"edit": True, "rowHeight": 32}`). |
|
|
75
|
+
| `licence` | in | Lattice Grid licence key. Empty on localhost → free/unwatermarked. |
|
|
76
|
+
| `cellChanged` | **out** | Last manual edit `{key, colId, value, old, ts}`. Drives Python callbacks. |
|
|
77
|
+
| `selectedKeys` | in/out | Selected row keys; set by the grid, settable from Python. |
|
|
78
|
+
| `licenceState` | **out** | Grid's resolved licence state (e.g. `"localhost"`). |
|
|
79
|
+
| `style`, `className` | in | Host element styling. |
|
|
80
|
+
|
|
81
|
+
## Licence plumbing
|
|
82
|
+
|
|
83
|
+
The `licence` prop is a single opaque string handed straight to
|
|
84
|
+
`createGrid({ licence })`; the grid resolves it **client-side** (localhost origins
|
|
85
|
+
run free, unwatermarked, with no key). The resolved state is surfaced back on the
|
|
86
|
+
`licenceState` prop. This is the same contract as the Jupyter widget — the shared
|
|
87
|
+
rules live in `lattice_grid_pandas._grid`.
|
|
88
|
+
|
|
89
|
+
## Grid delivery: vendored (default) vs CDN
|
|
90
|
+
|
|
91
|
+
The grid JavaScript can reach the browser two ways. **This package implements the
|
|
92
|
+
vendored path**; the CDN path is documented here for operators who prefer it.
|
|
93
|
+
|
|
94
|
+
### Vendored (implemented)
|
|
95
|
+
|
|
96
|
+
`npm run build` bundles the grid core + React adapter into
|
|
97
|
+
`lattice_grid_dash/lattice_grid_dash.min.js` (~2.7 MB) via `esbuild`, and the grid
|
|
98
|
+
CSS is vendored as `lattice_grid_dash/lattice-grid.min.css`. Both are registered
|
|
99
|
+
as Dash assets (`_js_dist` / `_css_dist`), so the component works **fully offline**
|
|
100
|
+
— no network at render time. This mirrors the C1 widget's `offline=True` default
|
|
101
|
+
and is what the browser smoke test exercises.
|
|
102
|
+
|
|
103
|
+
Rebuild:
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cd packages/lattice-grid-dash
|
|
107
|
+
npm install
|
|
108
|
+
npm run build # build:js (esbuild) + build:py (dash-generate-components)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### CDN (documented alternative)
|
|
112
|
+
|
|
113
|
+
To ship a thin component bundle that loads the grid from jsDelivr at render time
|
|
114
|
+
(smaller wheel, needs network + a relaxed CSP):
|
|
115
|
+
|
|
116
|
+
1. In `build.mjs`, mark the grid packages external:
|
|
117
|
+
`external: ['react-dom', '@toclocoinc/lattice-grid', '@toclocoinc/lattice-grid/modules/react']`.
|
|
118
|
+
2. Before the bundle loads, inject the grid from the CDN, e.g.
|
|
119
|
+
`https://cdn.jsdelivr.net/npm/@toclocoinc/lattice-grid@1.40.0/lattice-grid.esm.min.js`
|
|
120
|
+
and `.../modules/react.esm.min.js` (URLs available from
|
|
121
|
+
`lattice_grid_pandas.cdn_urls()`), and add the stylesheet
|
|
122
|
+
`.../lattice-grid.min.css`.
|
|
123
|
+
3. Drop the vendored files from `_js_dist` / `_css_dist`.
|
|
124
|
+
|
|
125
|
+
The two are mutually exclusive; pick one per build.
|
|
126
|
+
|
|
127
|
+
## Development / build toolchain
|
|
128
|
+
|
|
129
|
+
- `npm run build:js` — `esbuild` bundles `src/lib/bundle.js` → `lattice_grid_dash/lattice_grid_dash.min.js`. `react` is aliased to Dash's global React (`src/lib/react-shim.js`); react/react-dom are never bundled.
|
|
130
|
+
- `npm run build:py` — `dash-generate-components` (react-docgen v5) reads `src/lib/components/LatticeGrid.react.js` and generates `LatticeGrid.py`, `_imports_.py`, and `metadata.json`.
|
|
131
|
+
- `pytest tests/` — unit tests for the data bridge, plus a real-browser Playwright smoke test (system Chrome via `channel='chrome'`).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the browser bundle for the Dash component.
|
|
3
|
+
*
|
|
4
|
+
* Output: lattice_grid_dash/lattice_grid_dash.min.js (an IIFE that registers
|
|
5
|
+
* window.lattice_grid_dash.LatticeGrid).
|
|
6
|
+
*
|
|
7
|
+
* DELIVERY = "vendor" (implemented): the grid core + its React adapter are
|
|
8
|
+
* bundled in, so the component works fully offline with no CDN at render time.
|
|
9
|
+
* See README for the "cdn" delivery variant (mark the grid packages external and
|
|
10
|
+
* load them from jsDelivr at runtime) -- documented, not built here.
|
|
11
|
+
*
|
|
12
|
+
* React/ReactDOM are NOT bundled: the component reads window.React (served by the
|
|
13
|
+
* dash-renderer), guaranteeing a single React instance.
|
|
14
|
+
*/
|
|
15
|
+
import {build} from 'esbuild';
|
|
16
|
+
import {mkdirSync} from 'node:fs';
|
|
17
|
+
import {fileURLToPath} from 'node:url';
|
|
18
|
+
import {dirname, resolve} from 'node:path';
|
|
19
|
+
|
|
20
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
+
mkdirSync('lattice_grid_dash', {recursive: true});
|
|
22
|
+
|
|
23
|
+
await build({
|
|
24
|
+
entryPoints: ['src/lib/bundle.js'],
|
|
25
|
+
bundle: true,
|
|
26
|
+
format: 'iife',
|
|
27
|
+
target: ['es2019'],
|
|
28
|
+
minify: true,
|
|
29
|
+
sourcemap: false,
|
|
30
|
+
outfile: 'lattice_grid_dash/lattice_grid_dash.min.js',
|
|
31
|
+
// `react` -> Dash's global React (see src/lib/react-shim.js); react-dom is
|
|
32
|
+
// unused by the grid/adapter, so exclude it entirely.
|
|
33
|
+
alias: {react: resolve(__dirname, 'src/lib/react-shim.js')},
|
|
34
|
+
external: ['react-dom'],
|
|
35
|
+
logLevel: 'info',
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
console.log('built lattice_grid_dash/lattice_grid_dash.min.js');
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Minimal Dash app demonstrating the LatticeGrid component round-trip.
|
|
2
|
+
|
|
3
|
+
Run: python examples/app.py then open http://127.0.0.1:8050
|
|
4
|
+
|
|
5
|
+
Edit a cell in the grid; the panel below updates from a Python @callback that
|
|
6
|
+
observes the component's `cellChanged` prop, and the DataFrame is updated
|
|
7
|
+
server-side (dtype-preserved) via `apply_cell_edit`.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from dash import Dash, Input, Output, callback, html
|
|
12
|
+
|
|
13
|
+
import lattice_grid_dash
|
|
14
|
+
from lattice_grid_dash import apply_cell_edit, dataframe_to_data
|
|
15
|
+
|
|
16
|
+
df = pd.DataFrame(
|
|
17
|
+
{
|
|
18
|
+
"name": ["Ada", "Grace", "Linus"],
|
|
19
|
+
"score": [91, 88, 77],
|
|
20
|
+
"active": [True, False, True],
|
|
21
|
+
}
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
app = Dash(__name__)
|
|
25
|
+
|
|
26
|
+
app.layout = html.Div(
|
|
27
|
+
[
|
|
28
|
+
html.H3("Lattice Grid in Dash"),
|
|
29
|
+
lattice_grid_dash.LatticeGrid(
|
|
30
|
+
id="grid",
|
|
31
|
+
data=dataframe_to_data(df),
|
|
32
|
+
options={"edit": True},
|
|
33
|
+
),
|
|
34
|
+
html.Pre(id="out", children="Edit a cell..."),
|
|
35
|
+
],
|
|
36
|
+
style={"maxWidth": "720px", "margin": "2rem auto", "fontFamily": "system-ui"},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@callback(Output("out", "children"), Input("grid", "cellChanged"))
|
|
41
|
+
def on_edit(edit):
|
|
42
|
+
if not edit:
|
|
43
|
+
return "Edit a cell..."
|
|
44
|
+
apply_cell_edit(df, edit) # server-side DataFrame stays in sync + typed
|
|
45
|
+
return (
|
|
46
|
+
f"cellChanged -> row {edit['key']}, column '{edit['colId']}' = "
|
|
47
|
+
f"{edit['value']!r} (was {edit.get('old')!r})\n"
|
|
48
|
+
f"df row now: {df.iloc[int(edit['key'])].to_dict()}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
app.run(debug=True)
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# AUTO GENERATED FILE - DO NOT EDIT
|
|
2
|
+
|
|
3
|
+
import typing # noqa: F401
|
|
4
|
+
from typing_extensions import TypedDict, NotRequired, Literal # noqa: F401
|
|
5
|
+
from dash.development.base_component import Component, _explicitize_args
|
|
6
|
+
try:
|
|
7
|
+
from dash.types import NumberType # noqa: F401
|
|
8
|
+
except ImportError:
|
|
9
|
+
# Backwards compatibility for dash<=4.1.0
|
|
10
|
+
if typing.TYPE_CHECKING:
|
|
11
|
+
raise
|
|
12
|
+
NumberType = typing.Union[ # noqa: F401
|
|
13
|
+
typing.SupportsFloat, typing.SupportsInt, typing.SupportsComplex
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
ComponentSingleType = typing.Union[str, int, float, Component, None]
|
|
17
|
+
ComponentType = typing.Union[
|
|
18
|
+
ComponentSingleType,
|
|
19
|
+
typing.Sequence[ComponentSingleType],
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LatticeGrid(Component):
|
|
24
|
+
"""A LatticeGrid component.
|
|
25
|
+
LatticeGrid Dash component.
|
|
26
|
+
|
|
27
|
+
Keyword arguments:
|
|
28
|
+
|
|
29
|
+
- id (string; optional):
|
|
30
|
+
Component id, used to target the component in Dash callbacks.
|
|
31
|
+
|
|
32
|
+
- cellChanged (dict; optional):
|
|
33
|
+
OUTPUT. The last cell edit made in the grid: `{key, colId, value,
|
|
34
|
+
old, ts}`. Updated on every manual edit so a Python `@callback`
|
|
35
|
+
with `Input(id, \"cellChanged\")` fires. Read-only from Python.
|
|
36
|
+
|
|
37
|
+
- className (string; optional):
|
|
38
|
+
CSS class for the grid host element.
|
|
39
|
+
|
|
40
|
+
- columns (list; optional):
|
|
41
|
+
Optional explicit column definitions; overrides `data.columns`.
|
|
42
|
+
|
|
43
|
+
- data (dict; default {columns: [], rowKey: ROW_KEY, columnar: {}}):
|
|
44
|
+
Columnar, DataFrame-derived data produced by
|
|
45
|
+
`lattice-grid-pandas`: `{columns: [...], rowKey: \"__row_id__\",
|
|
46
|
+
columnar: {field: [values...]}}`. Rows are reconstructed in the
|
|
47
|
+
browser (virtualized rendering).
|
|
48
|
+
|
|
49
|
+
- licence (string; default ''):
|
|
50
|
+
Lattice Grid licence key. Empty on localhost runs
|
|
51
|
+
free/unwatermarked.
|
|
52
|
+
|
|
53
|
+
- licenceState (string; optional):
|
|
54
|
+
OUTPUT. The grid's resolved licence state (e.g. \"localhost\",
|
|
55
|
+
\"valid\").
|
|
56
|
+
|
|
57
|
+
- options (dict; optional):
|
|
58
|
+
Grid options passed through to `createGrid` (e.g. edit,
|
|
59
|
+
rowHeight).
|
|
60
|
+
|
|
61
|
+
- selectedKeys (list of strings; optional):
|
|
62
|
+
Selected row keys. Set by the grid on selection; may also be set
|
|
63
|
+
from Python."""
|
|
64
|
+
_children_props: typing.List[str] = []
|
|
65
|
+
_base_nodes = ['children']
|
|
66
|
+
_namespace = 'lattice_grid_dash'
|
|
67
|
+
_type = 'LatticeGrid'
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
id: typing.Optional[typing.Union[str, dict]] = None,
|
|
73
|
+
data: typing.Optional[dict] = None,
|
|
74
|
+
columns: typing.Optional[typing.Sequence] = None,
|
|
75
|
+
options: typing.Optional[dict] = None,
|
|
76
|
+
licence: typing.Optional[str] = None,
|
|
77
|
+
cellChanged: typing.Optional[dict] = None,
|
|
78
|
+
selectedKeys: typing.Optional[typing.Sequence[str]] = None,
|
|
79
|
+
licenceState: typing.Optional[str] = None,
|
|
80
|
+
style: typing.Optional[typing.Any] = None,
|
|
81
|
+
className: typing.Optional[str] = None,
|
|
82
|
+
**kwargs
|
|
83
|
+
):
|
|
84
|
+
self._prop_names = ['id', 'cellChanged', 'className', 'columns', 'data', 'licence', 'licenceState', 'options', 'selectedKeys', 'style']
|
|
85
|
+
self._valid_wildcard_attributes = []
|
|
86
|
+
self.available_properties = ['id', 'cellChanged', 'className', 'columns', 'data', 'licence', 'licenceState', 'options', 'selectedKeys', 'style']
|
|
87
|
+
self.available_wildcard_properties = []
|
|
88
|
+
_explicit_args = kwargs.pop('_explicit_args')
|
|
89
|
+
_locals = locals()
|
|
90
|
+
_locals.update(kwargs) # For wildcard attrs and excess named props
|
|
91
|
+
args = {k: _locals[k] for k in _explicit_args}
|
|
92
|
+
|
|
93
|
+
super(LatticeGrid, self).__init__(**args)
|
|
94
|
+
|
|
95
|
+
setattr(LatticeGrid, "__init__", _explicitize_args(LatticeGrid.__init__))
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""lattice-grid-dash -- a Dash component wrapping Lattice Grid.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
import lattice_grid_dash
|
|
6
|
+
from dash import Dash, callback, Input, Output, html
|
|
7
|
+
import pandas as pd
|
|
8
|
+
from lattice_grid_dash import dataframe_to_data
|
|
9
|
+
|
|
10
|
+
df = pd.DataFrame({"name": ["Ada", "Grace"], "score": [91, 88]})
|
|
11
|
+
app = Dash(__name__)
|
|
12
|
+
app.layout = html.Div([
|
|
13
|
+
lattice_grid_dash.LatticeGrid(id="grid", data=dataframe_to_data(df)),
|
|
14
|
+
html.Div(id="out"),
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
@callback(Output("out", "children"), Input("grid", "cellChanged"))
|
|
18
|
+
def show(edit):
|
|
19
|
+
return f"edited {edit}" if edit else "no edits yet"
|
|
20
|
+
|
|
21
|
+
The grid JS is *vendored* into this package (``lattice_grid_dash.min.js``), so it
|
|
22
|
+
works offline with no CDN at render time. See the README for the CDN variant.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
import json as _json
|
|
26
|
+
import os as _os
|
|
27
|
+
import sys as _sys
|
|
28
|
+
|
|
29
|
+
import dash as _dash
|
|
30
|
+
|
|
31
|
+
from ._imports_ import * # noqa: F401,F403
|
|
32
|
+
from ._imports_ import __all__ # noqa: F401
|
|
33
|
+
from ._serialize_bridge import dataframe_to_data, apply_cell_edit # noqa: F401
|
|
34
|
+
|
|
35
|
+
__all__ = list(__all__) + ["dataframe_to_data", "apply_cell_edit"]
|
|
36
|
+
|
|
37
|
+
if not hasattr(_dash, "development"):
|
|
38
|
+
print(
|
|
39
|
+
"Dash was not successfully imported. Make sure you don't have a file "
|
|
40
|
+
'named \n"dash.py" in your current directory.',
|
|
41
|
+
file=_sys.stderr,
|
|
42
|
+
)
|
|
43
|
+
_sys.exit(1)
|
|
44
|
+
|
|
45
|
+
_basepath = _os.path.dirname(__file__)
|
|
46
|
+
_filepath = _os.path.abspath(_os.path.join(_basepath, "package-info.json"))
|
|
47
|
+
with open(_filepath) as f:
|
|
48
|
+
package = _json.load(f)
|
|
49
|
+
|
|
50
|
+
package_name = package["name"].replace(" ", "_").replace("-", "_")
|
|
51
|
+
__version__ = package["version"]
|
|
52
|
+
|
|
53
|
+
_current_path = _os.path.dirname(_os.path.abspath(__file__))
|
|
54
|
+
|
|
55
|
+
_this_module = _sys.modules[__name__]
|
|
56
|
+
|
|
57
|
+
_js_dist = [
|
|
58
|
+
{
|
|
59
|
+
"relative_package_path": "lattice_grid_dash.min.js",
|
|
60
|
+
"namespace": package_name,
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
_css_dist = [
|
|
65
|
+
{
|
|
66
|
+
"relative_package_path": "lattice-grid.min.css",
|
|
67
|
+
"namespace": package_name,
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
for _component in __all__:
|
|
72
|
+
if hasattr(_this_module, _component):
|
|
73
|
+
_comp = getattr(_this_module, _component)
|
|
74
|
+
if isinstance(_comp, type):
|
|
75
|
+
setattr(_comp, "_js_dist", _js_dist)
|
|
76
|
+
setattr(_comp, "_css_dist", _css_dist)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""DataFrame <-> ``LatticeGrid`` data-prop bridge for Dash apps.
|
|
2
|
+
|
|
3
|
+
This is a *thin* adapter over ``lattice-grid-pandas`` -- the same package the
|
|
4
|
+
Jupyter widget (phase C1) uses. It adds ZERO serialization logic of its own: the
|
|
5
|
+
dtype mapping, the columnar payload build and the edit dtype-coercion all come
|
|
6
|
+
from the shared package, so there is one implementation across both wrappers.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Iterable, Optional
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from lattice_grid_pandas import (
|
|
15
|
+
ROW_KEY,
|
|
16
|
+
build_columnar,
|
|
17
|
+
build_columns,
|
|
18
|
+
cast_to_dtype,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def positional_keys(n: int) -> list[str]:
|
|
23
|
+
"""Stable per-row keys: the row's integer position, as a string ('0'..'n-1').
|
|
24
|
+
|
|
25
|
+
Matches the Jupyter widget's key scheme, so both wrappers behave identically.
|
|
26
|
+
"""
|
|
27
|
+
return [str(i) for i in range(n)]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def dataframe_to_data(
|
|
31
|
+
df: pd.DataFrame,
|
|
32
|
+
keys: Optional[Iterable[str]] = None,
|
|
33
|
+
) -> dict:
|
|
34
|
+
"""Build the ``data`` prop for ``LatticeGrid`` from a DataFrame.
|
|
35
|
+
|
|
36
|
+
Returns ``{"columns": [...], "rowKey": "__row_id__", "columnar": {...}}`` where
|
|
37
|
+
the browser reconstructs row objects from the column-major ``columnar`` block.
|
|
38
|
+
"""
|
|
39
|
+
if not isinstance(df, pd.DataFrame):
|
|
40
|
+
raise TypeError("dataframe_to_data expects a pandas DataFrame")
|
|
41
|
+
key_list = list(keys) if keys is not None else positional_keys(len(df))
|
|
42
|
+
if len(key_list) != len(df):
|
|
43
|
+
raise ValueError("keys length must match the number of rows")
|
|
44
|
+
return {
|
|
45
|
+
"columns": build_columns(df),
|
|
46
|
+
"rowKey": ROW_KEY,
|
|
47
|
+
"columnar": build_columnar(df, key_list),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def apply_cell_edit(
|
|
52
|
+
df: pd.DataFrame,
|
|
53
|
+
edit: Optional[dict],
|
|
54
|
+
keys: Optional[Iterable[str]] = None,
|
|
55
|
+
) -> pd.DataFrame:
|
|
56
|
+
"""Apply one ``cellChanged`` payload back into a DataFrame, dtype-preserved.
|
|
57
|
+
|
|
58
|
+
``edit`` is the ``{"key", "colId", "value"}`` payload the component emits.
|
|
59
|
+
Returns ``df`` mutated in place (and also returned, for convenience). Edits to
|
|
60
|
+
read-only index columns or unknown rows are ignored, mirroring the widget.
|
|
61
|
+
"""
|
|
62
|
+
if not edit:
|
|
63
|
+
return df
|
|
64
|
+
col = edit.get("colId")
|
|
65
|
+
key = edit.get("key")
|
|
66
|
+
value = edit.get("value")
|
|
67
|
+
if col not in df.columns:
|
|
68
|
+
return df # index columns are read-only; ignore
|
|
69
|
+
key_list = list(keys) if keys is not None else positional_keys(len(df))
|
|
70
|
+
try:
|
|
71
|
+
pos = key_list.index(str(key))
|
|
72
|
+
except ValueError:
|
|
73
|
+
return df
|
|
74
|
+
value = cast_to_dtype(df[col].dtype, value)
|
|
75
|
+
df.iat[pos, df.columns.get_loc(col)] = value
|
|
76
|
+
return df
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
__all__ = ["dataframe_to_data", "apply_cell_edit", "positional_keys", "ROW_KEY"]
|