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.
@@ -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,5 @@
1
+ from .LatticeGrid import LatticeGrid
2
+
3
+ __all__ = [
4
+ "LatticeGrid"
5
+ ]
@@ -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"]