Lattice Grid

python and pandas

Python & Pandas Data Grid for Jupyter and Dash

Turn a DataFrame into an application. Bring pandas data into a high-performance interactive grid for Jupyter and Dash. Filter it. Group it. Edit it. Analyse it. Without turning your DataFrame into frontend plumbing.

from lattice_grid_jupyter import LatticeGridWidget

grid = LatticeGridWidget(df)
grid

That is the whole thing. Three lines in a cell.

pip install lattice-grid-jupyter
Building…
Loading a live grid…

Open the full demo → Build it yourself →

what the demo is, and is not

A book of fifty thousand positions, on the screen you would build.

Portfolio value, unrealised profit and loss, and value at risk. Exposure by sector. A sector summary built from the positions themselves. And the positions, sorted, filtered and editable. Pick a sector and every figure recomputes over the holdings that are left, which is how the one sector dragging the book down turns up in a click.

Be clear about what you are looking at: this is the grid running in your browser, not a notebook running on our server. In Jupyter the positions grid and the sector grid are what the widget puts in your cells, and the figures beside them are the pandas reductions you already write. Edit a quantity or a price in the demo and the panel underneath prints the cell output that same edit gives you in your own notebook.

typed both ways

Your DataFrame isn't JSON.

The usual way to get a frame in front of a browser is a list of records: one dictionary per row, every field name repeated, every value flattened to whatever survives the trip. A frame handed over that way arrives as a table of strings, and the work of putting the types back lands on you.

Lattice reads the frame as pandas holds it. Your dtypes decide the column types, so numbers arrive as numbers, booleans as booleans and timestamps as timestamps. The frame travels a column at a time rather than a row at a time. And when someone edits a cell, the value goes back into the frame cast to that column's dtype, so a whole-number column is still a whole-number column afterwards.

Your dtypes are the schema

You declare nothing. Numbers, booleans and timestamps come through as themselves, with the sorting, filtering and formatting that go with each, and everything else reads as text.

Column by column

The frame goes over the way it is stored, a column at a time, and only the rows on screen are ever drawn. Nothing is rebuilt into a dictionary per row on the way.

Edits come back typed

An edit is cast to the column's dtype on the way into the frame, so a whole-number column comes back whole numbers and a decimal column comes back decimals. There is no parsing step for you to write.

Missing stays missing

Whichever way your frame spells an absent value, it shows as a blank cell rather than as the word for it printed in the middle of your data.

Your index comes with it

A named index, including a multi-level one, arrives as leading columns you can read, so the labels you have been working with are still in front of you.

An edit lands on the right row

Rows are identified by where they sit, not by their label, so a frame with repeated index labels edits correctly instead of writing your change into the wrong one.

one grid, two homes

Prototype in Jupyter. Ship in Dash.

Work starts in a notebook, because that is where the frame is. It ends somewhere a colleague can open in a browser. The usual cost of that journey is rebuilding the table: a notebook widget for you, a different component for the app, two sets of column definitions and two ideas about what an edit means.

Here it is one grid with two front doors. The same dtype reading, the same column types, the same typed round trip. What changes is who is watching the edit: in the notebook it is you, reading the frame back; in Dash it is a callback.

start here

Explore a frame without leaving the cell.

Show it as a grid, fix values by hand, sort and filter to find what needs attention, and pick the edited frame straight back up from the widget. Changes flow both ways, so you can push new rows or replace the frame from Python and the grid repaints.

It renders wherever ipywidgets do: JupyterLab, the classic Notebook, VS Code and Colab. On your own machine it runs free with no key.

pip install lattice-grid-jupyter

lattice-grid-jupyter on PyPI →

import pandas as pd
from lattice_grid_jupyter import LatticeGridWidget

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

grid = LatticeGridWidget(df)   # an editable grid over your DataFrame
grid                           # show it in a notebook cell

# edit cells in the grid, then read them back in Python, dtypes preserved:
grid.df

# push changes the other way, from Python into the grid:
grid.append_rows([{"name": "Edsger", "score": 95, "active": True}])
grid.set_data(df)              # or replace the whole frame and repaint

then ship it

The same grid, with your callbacks behind it.

Put the grid over a DataFrame in a Dash layout and let your callbacks respond to what a user changes. Each edit and the current selection arrive as ordinary props, so a cell change drives a Python callback the same way a dropdown or a slider does, and one call keeps the server-side frame in step and typed.

The grid ships inside the package, so it works with no build step and with no extra request at render time. On localhost it runs free with no key.

pip install lattice-grid-dash

lattice-grid-dash on PyPI →

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

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

app = Dash(__name__)
app.layout = html.Div([
    lattice_grid_dash.LatticeGrid(
        id="grid",
        data=dataframe_to_data(df),
        options={"edit": True},
    ),
    html.Pre(id="out"),
])

@callback(Output("out", "children"), Input("grid", "cellChanged"))
def on_edit(edit):
    if not edit:
        return "Edit a cell to see it here."
    apply_cell_edit(df, edit)          # keep the server-side DataFrame in sync, typed
    return f"{edit['colId']} on row {edit['key']} is now {edit['value']}"

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

what to expect

What the Python packages do, and what they leave to you.

They put an editable, sortable, filterable grid over a pandas DataFrame and carry every edit back into it typed. That is the job, and it is the whole job. The charts, the stat tiles and the derived summary in the demo above are the browser-side grid at work; in a notebook you compute those figures in pandas beside the widget and put a grouped frame into a grid of its own.

These are early packages. The grid they put in your cell is the same product the rest of this site is built on; the Python layer wrapped around it is new, and it does what is described here rather than more. Both run free and unwatermarked on your own machine, so you can find out whether they fit before anything is decided.

Everything the two packages expose →

task guides

Five things you are probably here to do.

Each one is a single task, with the complete runnable file and, where it is Python, the real output from actually running it.

notebooks

Eight Jupyter notebooks you can run today.

Each notebook installs with one pip line, fetches its own public data and runs top to bottom with no licence key, in Google Colab or JupyterLab. The code is MIT-licensed and every notebook names the licence of its data.

All eight on GitHub →

  • 12.6 million taxi trips in one grid

    A 12.6-million-row dataframe in the large-dataframe mode: header histograms, a filter to long airport trips, sort, then the selection back as a pandas DataFrame.

    Data NYC taxi trips, January to April 2024 (public Parquet, NYC Taxi & Limousine Commission open data)

  • Where a classifier goes wrong

    A colour scale on the error, an actual by predicted pivot, groups by sex, race and age band, and a ROC chart that redraws for the segment you pick.

    Data UCI Adult census income (CC BY 4.0)

  • A feature table from four DataFrames

    Customers, transactions, attributes and churn labels joined, rolled up and type-coerced into one live feature table, with no pandas merge, groupby or pivot.

    Data Seeded synthetic retail data, generated in the notebook (MIT)

  • Forecast versus actual

    An ETS forecast with 80% intervals over monthly retail sales, a residual colour scale, and fan and seasonal-decomposition charts that redraw over the rows you select.

    Data FRED RSXFSN, US retail sales (US Census Bureau, public domain)

  • Compare 30 training runs

    One row per run with its hyperparameters, metrics under colour scales, a loss-curve sparkline in every row, the best run pinned to the top, and the ticked runs back as a DataFrame.

    Data Seeded scikit-learn sweep on UCI handwritten digits (CC BY 4.0)

  • Label a text dataset by keyboard

    A label column that is a dropdown, validation that refuses anything else, a Needs review filter, a bar chart that moves as you type, and the labelled DataFrame back.

    Data UCI SMS Spam Collection (CC BY 4.0)

  • From a DuckDB query to a grid

    Query a Parquet file with DuckDB, see the result profiled in the grid, then change the query and call set_data: widths, sort and filters stay in place.

    Data NYC taxi trips, January 2024 (public Parquet, NYC Taxi & Limousine Commission open data)

  • 4.8 million food products over DuckDB

    The grid opens a DuckDB relation in the large-dataframe mode, so every sort, filter, histogram and count runs as SQL, with a Nutri-Score chart that follows the filter.

    Data Open Food Facts (ODbL database, DbCL contents); a 100,000-product sample setting is built in

Go further than a table in the notebook.

Select rows and get a DataFrame back. Profile a column with histograms and a statistics panel. Scroll ten million rows. Chart the grid's own data, and join several DataFrames with the data router. Each has a short example in the tutorial.

Install it and open a DataFrame.

Pick the package for where you work, point it at a frame, and see it as a grid in a minute. It runs free on your own machine, so you can try it before you decide anything.