Lattice Grid Buy a licence

get started

Hello grid in Python: your first pandas data grid

Start with a DataFrame you already have and finish with a grid you can scroll, sort, filter and edit, in the notebook or in a Dash app. Follow the steps in order and you will have one running in a few minutes.

There are two ways to work, and this page covers both. Start with the notebook widget if you live in Jupyter or VS Code. Move to the Dash track when you want the same grid inside a web app your whole team can open. The two share the same DataFrame handling, so what you learn on one carries straight over to the other.

In a notebook: prerequisites

You need Python 3.9 or newer with pip, and a notebook environment to run cells in. If you do not already have one, the install step below brings in JupyterLab for you. If you prefer VS Code, use its built-in notebook support with the same interpreter. The widget renders wherever ipywidgets do, so any of those is fine. On your own machine it runs free with no key.

pandas 1.5 or newer is what the grid reads its data from. You do not have to install it first; the next step brings it in for you.

1. Install JupyterLab and the grid widget

One command installs JupyterLab and the grid widget together. It brings in everything the widget needs to render in a notebook, including anywidget, ipywidgets, pandas and numpy, alongside the shared pandas layer both Python packages use.

pip install jupyterlab lattice-grid-jupyter

2. Start a notebook

Launch JupyterLab from the same environment you installed into, then create a new notebook, for example hello_grid.ipynb.

jupyter lab

If you work in VS Code instead, open a new .ipynb file and pick the same Python interpreter you installed the package into. Either way, you now have an empty cell to type in.

3. Render a DataFrame

Paste this into the first cell and run it. It builds a small DataFrame and hands it to LatticeGridWidget. Evaluating the widget on the last line of the cell is what draws it: a notebook shows the value of a cell's final expression, and here that value is the interactive grid.

import pandas as pd
from lattice_grid_jupyter import LatticeGridWidget

df = pd.DataFrame({"name": ["Ada", "Grace"], "score": [91, 88]})
grid = LatticeGridWidget(df)   # evaluate the widget in a cell to render it
grid                           # anywidget renders the interactive grid in the notebook

You should see a grid with two rows, Ada and Grace, and two columns, name and score. Click a column header to sort, and open a column's filter to narrow the rows. The score column arrives as a number, not text, because the grid follows your DataFrame's dtypes.

4. Edit a cell, then read it back

Double-click a score in the grid, type a new number, and commit the edit. Now read the frame back in a new cell:

grid.df   # read-only property -> the live DataFrame (reflects your edits)

grid.df is the live frame, so it already carries your edit. The value comes back cast to the column's dtype, so a number stays a number: an integer column stays an integer, not a string you would have to parse. There is no export step and no second copy to keep in step by hand.

5. Change it from Python too

The grid works both ways. As well as editing in the browser, you can drive it from code, and the grid repaints to match. Each call below is one you can run in a cell.

grid.apply_edit(key="0", col_id="score", value=100)   # returns None; row key "0" score -> 100 (int64 preserved)
grid.append_rows([{"name": "Alan", "score": 77}])      # returns ['2'] (the new row key(s))
grid.delete_rows(["2"])                                # returns the list of row keys removed
grid.set_data(pd.DataFrame({"name": ["X"], "score": [1]}))   # returns None; replaces the frame and repaints
  • apply_edit(key, col_id, value) sets one cell by its row key and column, and returns nothing.
  • append_rows(rows) adds rows from a list of dicts or a DataFrame, and returns the new row keys as strings.
  • delete_rows(keys) removes rows by key, and returns the list of keys it removed.
  • set_data(df) replaces the whole frame and repaints, and returns nothing.

Row keys are strings, starting at "0". Read the current frame at any point from grid.df.

6. Get the selection and the filtered view back

What you do in the grid comes back to Python as DataFrames. Click a row, Ctrl-click another, then read selected in the next cell. Type in the grid's search box and view is the matching rows, in the order you see them, so exporting what is on screen is one line.

w = LatticeGridWidget(df)
w                       # click a row, Ctrl-click another

w.selected              # the selected rows: a DataFrame with your original index and dtypes
w.view                  # the rows passing the grid's filters, in the grid's sort order
w.view.to_csv("shown.csv")   # export exactly what is on screen

def on_select(change):
    print(len(change["new"]), "rows selected")

w.observe(on_select, names="selected")

selected keeps your original index and dtypes, and is an empty frame when nothing is selected. Both update once per change you make, not once per keystroke. w.state holds the filter, sort, column, group and pivot state as a plain dict you can save and set again later.

7. Histograms, profiling and pivot

Pass grid options straight from Python. Header histograms show each column's distribution and a click on a bar filters on it. The Statistics panel profiles a column as you click its cells, and follows your filters. Mistyped options come back as Python warnings by name, so a typo never fails silently.

w = LatticeGridWidget(
    df,
    options={"row_height": 32, "stripe": True},
    columns=[{"field": "score", "title": "Score", "width": 120}],
    histograms=True,   # a distribution in every column header
    profile=True,      # the Statistics panel
    pivot="region",    # pivot on a field
)
w.options = {"row_height": 44}   # updates the live grid

8. Ten million rows

From 100,000 rows up, the widget no longer sends the frame to the browser. The grid asks Python for each window of rows as you scroll, and sorting, filtering, search, group subtotals, filter-menu values, histograms and profiles are computed over the whole frame. Parquet files and Polars frames open the same way.

pip install "lattice-grid-jupyter[large]"
w = LatticeGridWidget(big_df)                          # 10M rows: scrolls in windows automatically
w = LatticeGridWidget("events.parquet")                # a Parquet file, read by DuckDB
w = LatticeGridWidget(polars_df)                       # a Polars frame

v = w.view          # the rows passing the grid's filters, as a lazy handle
len(v)              # how many rows that is
v.head(20)          # the first 20, in the grid's sort order, as a DataFrame
v.slice(1000, 1100) # any range of the view

9. Charts in the notebook

LatticeChart draws a chart of the grid's own data. It follows the grid's filters, search and selection live in the browser, with no round trip to Python. You can also chart a DataFrame directly.

from ipywidgets import HBox
from lattice_grid_jupyter import LatticeGridWidget, LatticeChart

grid = LatticeGridWidget(df)
roc = LatticeChart(grid, type="roc", label="churned", score="score")
HBox([grid, roc])        # filter the grid: the chart redraws

10. The data router in the notebook

Declare several DataFrames as sources and let the grid join them: look up a label, collect child rows, roll totals up onto the parent row, spread attribute rows into columns. Updating a source refreshes only the rows it touches, and view hands the shaped rows back as a DataFrame, so a feature table goes straight into a model.

from lattice_grid_jupyter import LatticeGridWidget, LatticeRouter

router = LatticeRouter(
    sources={"customer": {"data": customers, "key": "customer_id",
                          "join": [{"from": "txn", "many": True, "foreign_key": "customer_id",
                                    "aggregate": {"spend": {"fn": "sum", "field": "amount"}}}]},
             "txn": {"data": txns, "key": "txn_id"}},
    routes={"customer": "customer"},
)
w = LatticeGridWidget(router, route="customer")   # one row per customer, with spend rolled up
router.update("txn", new_txns)                    # only the affected customers refresh
w.view                                            # the shaped rows as a DataFrame

What next

That is a working notebook grid. When you want the same grid in a web app, follow the Dash track below. For every constructor argument and method, with its signature and return, see the Python reference.

In a Dash app: prerequisites

You need Python 3.9 or newer with pip. That is all: the Dash track below runs a small app on your own machine, where the grid renders free with no key.

1. Install the component

One command again. It brings in Dash and the shared pandas layer, so you have both the app framework and the grid after this step.

pip install lattice-grid-dash

2. Write app.py

Create a file called app.py and paste this in. It puts the grid over a DataFrame, and wires a callback so that every committed edit updates the server-side frame and flows back to the grid, cast to the column's dtype.

import pandas as pd
from dash import Dash, callback, Input, Output
import lattice_grid_dash
from lattice_grid_dash import dataframe_to_data, apply_cell_edit

df = pd.DataFrame({"name": ["Ada", "Grace"], "score": [91, 88]})

app = Dash(__name__)
app.layout = lattice_grid_dash.LatticeGrid(
    id="grid",
    data=dataframe_to_data(df),
    options={"edit": True},
)

@callback(Output("grid", "data"), Input("grid", "cellChanged"), prevent_initial_call=True)
def on_edit(edit):
    global df
    df = apply_cell_edit(df, edit)     # dtype preserved
    return dataframe_to_data(df)

if __name__ == "__main__":
    app.run(debug=True)

The grid's front-end code ships inside the package, so this works offline with no build step and no request to a CDN at render time.

3. Run it and edit a cell

Start the app from the same environment you installed into:

python app.py

Open the address Dash prints, http://127.0.0.1:8050/, in your browser. Double-click a score, type a new value and commit it. That edit arrives at the cellChanged prop, which runs your callback; apply_cell_edit applies it to the DataFrame with the column's dtype preserved, and dataframe_to_data sends the updated frame back to the grid. The DataFrame on the server and the grid in the browser stay in step, typed, with one round-trip.

Troubleshooting

  • An install or import fails. Check your Python is 3.9 or newer with python --version, and that you installed into the same environment you are running.
  • The notebook cell runs but no grid appears, or jupyter is not found. The widget needs JupyterLab and anywidget, which pip install jupyterlab lattice-grid-jupyter pulls in. Run that command, make sure the notebook kernel is the environment you installed into, then re-run the cell so the widget is the last expression in it.
  • The Dash app will not start on its port. If something already holds port 8050, stop it, or run the app on another port.

See also the Python data grid overview and the full Python reference.