Everything runs as marimo apps -- Python files with marimo.App() where each @app.cell is a reactive cell. When a UI element's value changes, marimo automatically re-runs all downstream cells that depend on it. No custom event wiring needed.
mo.vstack()/mo.hstack()-- stacking panels vertically/horizontally (used everywhere for arranging plots, controls, and save buttons)mo.md()-- markdown section headers and explanatory text
mo.ui.dropdown()-- model selection, subject/session pickers, regressor pickers, scoring keysmo.ui.checkbox()-- toggle plot options (fit lapse logistic, show weighted dots, data smooth)mo.ui.slider()/mo.ui.range_slider()-- number of quantiles, simulation counts, lag rangesmo.ui.radio()-- psychometric background mode, state assignment mode, model line modemo.ui.multiselect()-- subject filtering, model directory selectionmo.ui.run_button()-- trigger expensive operations (run fit, run simulations, pick random session)mo.ui.number()-- seed valuesmo.ui.dataframe()-- display DataFrames as interactive tablesmo.ui.table()-- display fit parameter tablesmo.ui.matplotlib()-- wrap a matplotlib axis as interactive (used for emission summary withdebounce=True)mo.ui.dictionary()-- group multiple dropdowns into a single reactive dict (model comparison picks)
mo.state()-- explicit get/set state pairs for things likelast_fit_clickcounter andsession_pick(subject+session selection that persists across random-session button clicks)mo.stop()-- conditional cell halt with a fallback display (e.g., "No fitted arrays found -- run the fit first")
mo.status.progress_bar()-- shown during model fitting
All three come from the glmhmmt package (or wigglystuff) and are bridged into marimo via mo.ui.anywidget().
A custom panel for model configuration with:
- Task dropdown
- "New Fit" / "Load Existing" tabs
- Clickable chip grids for subjects, emission regressors, and transition regressors
- K slider, tau slider, lapse checkbox + max slider
- A scrollable table of saved fits (click to load)
- Custom alias text input + "RUN FIT" button
- Dark mode support
Used in glmhmm.py, glmhmmt_analysis.py, and glm.py. Its value is parsed into a ModelCfg dataclass via ModelCfg.from_value().
The Python-side _update_options() method scans results/fits/ for saved configs and queries the task adapter for available column options.
An audio-equalizer-style UI for interactively editing emission weight coefficients:
- Vertical sliders per feature per choice channel (Left/Right)
- Per-feature live numeric readout while sliding
- Multi-channel layout with features side by side
- Smooth drag interaction (prevents re-render during drag)
Used in glmhmmt_analysis.py to tweak individual GLM weights per state/feature/side and see the psychometric curve update live.
There is also an older CoefTweakerWidget in widgets.py with similar vertical-slider functionality inlined as JS.
An inline draggable number rendered within text. Used in glmhmm.py for a posterior threshold control. Wrapped via wrap_anywidget().
make_plot_saver() creates save buttons that appear next to each figure. Call save_plot(fig, label, stem=...) and it renders a marimo button. When clicked, it saves the figure to results/ in the format specified by config.toml.
Used in every notebook and figure script -- the primary way plots get exported to disk.
Native marimo equivalents:
mo.ui.dropdownfor taskmo.ui.tabsfor New/Loadmo.ui.multiselectfor subjects, emission cols, transition colsmo.ui.sliderfor K, tau, lapse_maxmo.ui.checkboxfor lapse togglemo.ui.dataframeormo.ui.tablefor saved fitsmo.ui.textfor alias,mo.ui.run_buttonfor the fit trigger
The Python-side logic (scanning results/fits/, querying adapters for columns) would move into regular marimo cells.
What you lose: The chip-grid UI is nicer than a multiselect for quickly toggling 15+ regressors -- you can see all options at a glance and toggle individually. The whole thing is also a single cohesive panel rather than scattered widgets. But functionally, native marimo covers it completely.
Marimo doesn't have vertical sliders (mo.ui.slider is horizontal only). You could fake it with one horizontal slider per feature per channel, but:
- You'd need ~20-30 individual sliders arranged in a grid
- You lose the equalizer visual metaphor that makes it intuitive to compare weights across features
mo.ui.slidertriggers a full cell re-run on each change, while the anywidget batches updates locally in the browser until mouseup
This one genuinely benefits from being a custom widget.
A mo.ui.slider or mo.ui.number does the same thing. The only difference is that TangleSlider renders as an underlined number you drag within a sentence -- a stylistic choice, not functional.
| Widget | Replaceable? | Worth replacing? |
|---|---|---|
ModelManagerWidget |
Yes, fully | Maybe -- lose chip grid but gain simplicity and no JS maintenance |
CoefficientEditorWidget |
Not cleanly | No -- vertical slider grid with smooth drag has no native equivalent |
TangleSlider |
Yes, trivially | Yes -- mo.ui.slider does the same thing |