Skip to content
A

Edit layers

CPTViewer can show a manually editable layer column after the read-only interpretation columns: a place to draw your own layer interpretation next to the automated ones, directly in the widget. Every change syncs back to Python through the editedLayers trait, so the notebook always holds the current state.

The column is always there. While it is empty, it offers a click-to-start placeholder: one click creates a single classless layer spanning the whole sounding, ready to be split up.

To start from existing layers instead, set editedLayers when you build the viewer, or assign the trait later. A common seed is one of the read-only interpretations, with its labels mapped onto your soil classes:

to_class = {"sand mix": "sand", "silt mix": "silt", "organic clay": "clay"}
viewer.editedLayers = [
{
"top": l["top"],
"bottom": l["bottom"],
"class": to_class.get(l["label"], "clay"),
}
for l in robertson["layers"]
]

Assigning the trait replaces the whole column, so re-seeding discards any edits made so far.

  • To move a boundary, drag it. A drag stops at a minimum layer thickness, so layers cannot collapse.
  • To split a layer, click in the narrow lane on the column’s outer edge. While the pointer is in the lane, a dashed line previews where the new boundary would go.
  • To merge two layers, move the pointer near their boundary in that same lane and click the × it offers. The upper layer keeps its class.
  • To set the class of a layer, click the layer. A pie menu opens with one wedge per soil class: click a wedge, or walk the wedges with the arrow keys and push Enter. Escape or a click outside closes the menu.
  • As a fast path, press the layer, drag toward a wedge, and release.

The pie menu offers the entries of the soil_classes palette, which is also the single source of truth for the layer colors: each edited layer carries a class key referencing a palette entry by name, and that entry drives both the fill and the label. The default palette holds gravel, sand, silt, clay, and peat; override the trait to change the classes or colors project-wide.

The front end writes every edit back to editedLayers. Observe the trait to keep a copy in Python:

edited = []
def on_edit(change):
edited[:] = change["new"]
viewer.observe(on_edit, names="editedLayers")

In Jupyter this is the whole story: edited always holds the latest layers, and any later cell can read it.

marimo’s reactivity adds one trap. A widget is rebuilt whenever its cell reruns, and a cell reruns whenever something it reads changes. If the viewer cell read the edits reactively, every boundary drag would rerun the cell and rebuild the widget mid-gesture, and the column would reset under the pointer.

The pattern that works: keep the edits in a plain dict that the viewer cell reads without tracking, and mirror them into mo.state for the cells that display the result.

# in a cell of its own, so it runs once and survives viewer rebuilds
edited_store = {"layers": []}
get_edited, set_edited = mo.state([])
def on_edit(change):
edited_store["layers"] = change["new"]
set_edited(change["new"])
viewer = CPTViewer(
cpt_data,
interpretations=interpretations,
editedLayers=edited_store["layers"],
)
viewer.observe(on_edit, names="editedLayers")
viewer
pd.DataFrame(get_edited()) # a reactive reader, reruns on every edit

An edit now updates the store and the mirror, the table cell reruns, and the viewer cell does not. When something else does rebuild the viewer (a changed channel selection, a new file), it comes back with editedLayers=edited_store["layers"] and the edits survive.

Layer boundaries arrive in whatever vertical coordinate the viewer plots. If the notebook can switch between depth and NAP, store the edits canonically in depth below surface and convert only at the widget boundary, with from_vertical and to_vertical:

def on_edit(change):
edited_store["layers"] = [
{
**l,
"top": from_vertical(l["top"], offset, vertical),
"bottom": from_vertical(l["bottom"], offset, vertical),
}
for l in change["new"]
]

The edits then survive a coordinate switch: the viewer cell converts them back with to_vertical when it rebuilds, the same way it converts annotations and interpretation layers.