Skip to content

Instantly share code, notes, and snippets.

@morganmcg1
Created February 11, 2025 15:45
Show Gist options
  • Save morganmcg1/7e2b64feb889f1ce764552d2ef98c012 to your computer and use it in GitHub Desktop.
Save morganmcg1/7e2b64feb889f1ce764552d2ef98c012 to your computer and use it in GitHub Desktop.
marimo_question.txt
This file has been truncated, but you can view the full file.
<file_tree>
/Users/morganmcguire/ML/marimo
├── .github
├── ├── ISSUE_TEMPLATE
├── └── workflows
├── configs
├── dagger
├── └── src
├── └── └── main
├── development_docs
├── docker
├── docs
├── ├── _static
├── ├── ├── js
├── ├── └── motherduck
├── ├── api
├── ├── ├── inputs
├── ├── ├── layouts
├── ├── └── media
├── ├── apps
├── ├── getting_started
├── ├── guides
├── ├── ├── coming_from
├── ├── ├── configuration
├── ├── ├── deploying
├── ├── ├── editor_features
├── ├── ├── integrating_with_marimo
├── ├── ├── publishing
├── ├── ├── └── community_cloud
├── ├── ├── testing
├── ├── └── working_with_data
├── ├── integrations
├── ├── overrides
├── └── stylesheets
├── examples
├── ├── ai
├── ├── ├── chat
├── ├── ├── data
├── ├── ├── misc
├── ├── └── tools
├── ├── cloud
├── ├── ├── gcp
├── ├── └── modal
├── ├── └── └── nbs
├── ├── control_flow
├── ├── frameworks
├── ├── ├── fastapi
├── ├── ├── └── templates
├── ├── ├── fastapi-github
├── ├── ├── └── templates
├── ├── ├── fasthtml
├── ├── └── flask
├── ├── └── └── templates
├── ├── layouts
├── ├── └── layouts
├── ├── markdown
├── ├── misc
├── ├── └── public
├── ├── sql
├── ├── └── misc
├── ├── testing
├── ├── third_party
├── ├── ├── aframe
├── ├── ├── anywidget
├── ├── ├── cvxpy
├── ├── ├── └── signals
├── ├── ├── └── ├── assets
├── ├── ├── └── └── modules
├── ├── ├── duckdb
├── ├── ├── great_tables
├── ├── ├── huggingface
├── ├── ├── ibis
├── ├── ├── leafmap
├── ├── ├── matplotlib
├── ├── ├── motherduck
├── ├── ├── └── embeddings
├── ├── ├── nvidia
├── ├── ├── plotly
├── ├── ├── polars
├── ├── ├── pygwalker
├── ├── ├── pymde
├── ├── ├── sage
├── ├── ├── substrate
├── ├── └── unsloth
├── └── ui
├── frontend
├── ├── .storybook
├── ├── e2e-tests
├── ├── └── py
├── ├── └── └── layouts
├── ├── islands
├── ├── └── __demo__
├── ├── patches
├── ├── public
├── ├── └── files
├── └── src
├── └── ├── __tests__
├── └── ├── └── components
├── └── ├── └── └── editor
├── └── ├── └── └── └── cell
├── └── ├── assets
├── └── ├── components
├── └── ├── ├── app-config
├── └── ├── ├── audio
├── └── ├── ├── buttons
├── └── ├── ├── charts
├── └── ├── ├── chat
├── └── ├── ├── data-table
├── └── ├── ├── ├── __test__
├── └── ├── ├── ├── └── __snapshots__
├── └── ├── ├── ├── __tests__
├── └── ├── ├── ├── column-formatting
├── └── ├── ├── ├── column-wrapping
├── └── ├── ├── └── hooks
├── └── ├── ├── databases
├── └── ├── ├── └── icons
├── └── ├── ├── datasets
├── └── ├── ├── debug
├── └── ├── ├── debugger
├── └── ├── ├── dependency-graph
├── └── ├── ├── └── utils
├── └── ├── ├── editor
├── └── ├── ├── ├── __tests__
├── └── ├── ├── ├── actions
├── └── ├── ├── ├── ai
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── boundary
├── └── ├── ├── ├── cell
├── └── ├── ├── ├── └── code
├── └── ├── ├── ├── chrome
├── └── ├── ├── ├── ├── components
├── └── ├── ├── ├── ├── panels
├── └── ├── ├── ├── ├── └── outline
├── └── ├── ├── ├── └── wrapper
├── └── ├── ├── ├── └── └── __tests__
├── └── ├── ├── ├── code
├── └── ├── ├── ├── columns
├── └── ├── ├── ├── controls
├── └── ├── ├── ├── database
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── └── └── __snapshots__
├── └── ├── ├── ├── errors
├── └── ├── ├── ├── file-tree
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── header
├── └── ├── ├── ├── inputs
├── └── ├── ├── ├── links
├── └── ├── ├── ├── output
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── └── renderers
├── └── ├── ├── └── ├── grid-layout
├── └── ├── ├── └── ├── slides-layout
├── └── ├── ├── └── └── vertical-layout
├── └── ├── ├── └── └── ├── __tests__
├── └── ├── ├── └── └── └── sidebar
├── └── ├── ├── └── └── └── └── __tests__
├── └── ├── ├── export
├── └── ├── ├── find-replace
├── └── ├── ├── forms
├── └── ├── ├── └── __tests__
├── └── ├── ├── home
├── └── ├── ├── icons
├── └── ├── ├── layout
├── └── ├── ├── modal
├── └── ├── ├── pages
├── └── ├── ├── scratchpad
├── └── ├── ├── shortcuts
├── └── ├── ├── slides
├── └── ├── ├── sort
├── └── ├── ├── static-html
├── └── ├── ├── terminal
├── └── ├── ├── tracing
├── └── ├── ├── ui
├── └── ├── ├── utils
├── └── ├── └── variables
├── └── ├── core
├── └── ├── ├── ai
├── └── ├── ├── alerts
├── └── ├── ├── cells
├── └── ├── ├── └── __tests__
├── └── ├── ├── └── └── __snapshots__
├── └── ├── ├── codemirror
├── └── ├── ├── ├── __tests__
├── └── ├── ├── ├── └── __snapshots__
├── └── ├── ├── ├── ai
├── └── ├── ├── ├── cells
├── └── ├── ├── ├── compat
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── completion
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── config
├── └── ├── ├── ├── copilot
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── editing
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── find-replace
├── └── ├── ├── ├── go-to-definition
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── keymaps
├── └── ├── ├── ├── language
├── └── ├── ├── ├── ├── __tests__
├── └── ├── ├── ├── └── utils
├── └── ├── ├── ├── markdown
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── misc
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── placeholder
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── react-dom
├── └── ├── ├── ├── rtc
├── └── ├── ├── └── theme
├── └── ├── ├── └── └── __tests__
├── └── ├── ├── config
├── └── ├── ├── └── __tests__
├── └── ├── ├── datasets
├── └── ├── ├── └── __tests__
├── └── ├── ├── debugger
├── └── ├── ├── documentation
├── └── ├── ├── dom
├── └── ├── ├── └── __tests__
├── └── ├── ├── errors
├── └── ├── ├── └── __tests__
├── └── ├── ├── export
├── └── ├── ├── functions
├── └── ├── ├── hotkeys
├── └── ├── ├── └── __tests__
├── └── ├── ├── islands
├── └── ├── ├── ├── __tests__
├── └── ├── ├── ├── components
├── └── ├── ├── └── worker
├── └── ├── ├── kernel
├── └── ├── ├── └── __tests__
├── └── ├── ├── layout
├── └── ├── ├── network
├── └── ├── ├── └── __tests__
├── └── ├── ├── saving
├── └── ├── ├── slots
├── └── ├── ├── state
├── └── ├── ├── └── __tests__
├── └── ├── ├── static
├── └── ├── ├── └── __tests__
├── └── ├── ├── variables
├── └── ├── ├── └── __tests__
├── └── ├── ├── vscode
├── └── ├── ├── wasm
├── └── ├── ├── ├── __tests__
├── └── ├── ├── └── worker
├── └── ├── └── websocket
├── └── ├── └── └── __tests__
├── └── ├── css
├── └── ├── └── app
├── └── ├── fonts
├── └── ├── ├── Fira_Mono
├── └── ├── ├── KaTeX
├── └── ├── ├── Lora
├── └── ├── ├── └── static
├── └── ├── └── PT_Sans
├── └── ├── hooks
├── └── ├── └── __tests__
├── └── ├── plugins
├── └── ├── ├── core
├── └── ├── ├── └── __test__
├── └── ├── ├── impl
├── └── ├── ├── ├── __tests__
├── └── ├── ├── ├── anywidget
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── chat
├── └── ├── ├── ├── code
├── └── ├── ├── ├── common
├── └── ├── ├── ├── data-editor
├── └── ├── ├── ├── data-explorer
├── └── ├── ├── ├── ├── components
├── └── ├── ├── ├── ├── functions
├── └── ├── ├── ├── ├── queries
├── └── ├── ├── ├── └── state
├── └── ├── ├── ├── data-frames
├── └── ├── ├── ├── ├── forms
├── └── ├── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── ├── └── └── __snapshots__
├── └── ├── ├── ├── ├── python
├── └── ├── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── ├── └── └── __snapshots__
├── └── ├── ├── ├── └── utils
├── └── ├── ├── ├── └── └── __tests__
├── └── ├── ├── ├── panel
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── ├── plotly
├── └── ├── ├── ├── └── __tests__
├── └── ├── ├── └── vega
├── └── ├── ├── └── └── __tests__
├── └── ├── ├── └── └── └── __snapshots__
├── └── ├── └── layout
├── └── ├── └── ├── __test__
├── └── ├── └── ├── carousel
├── └── ├── └── └── mermaid
├── └── ├── stories
├── └── ├── └── __fixtures__
├── └── ├── theme
├── └── └── utils
├── └── └── ├── __tests__
├── └── └── └── json
├── └── └── └── └── __tests__
├── lsp
├── marimo
├── ├── _ai
├── ├── _ast
├── ├── _cli
├── ├── ├── config
├── ├── ├── convert
├── ├── ├── development
├── ├── └── export
├── ├── _config
├── ├── _convert
├── ├── _data
├── ├── _dependencies
├── ├── _islands
├── ├── _messaging
├── ├── _output
├── ├── ├── data
├── ├── ├── formatters
├── ├── └── md_extensions
├── ├── _plugins
├── ├── ├── core
├── ├── ├── stateless
├── ├── ├── ├── mpl
├── ├── ├── └── status
├── ├── └── ui
├── ├── └── ├── _core
├── ├── └── └── _impl
├── ├── └── └── ├── anywidget
├── ├── └── └── ├── charts
├── ├── └── └── ├── chat
├── ├── └── └── ├── dataframes
├── ├── └── └── ├── └── transforms
├── ├── └── └── ├── tables
├── ├── └── └── └── utils
├── ├── _pyodide
├── ├── _runtime
├── ├── ├── app
├── ├── ├── context
├── ├── ├── layout
├── ├── ├── output
├── ├── ├── packages
├── ├── ├── reload
├── ├── ├── runner
├── ├── └── utils
├── ├── _save
├── ├── └── loaders
├── ├── _server
├── ├── ├── ai
├── ├── ├── api
├── ├── ├── └── endpoints
├── ├── ├── export
├── ├── ├── files
├── ├── ├── models
├── ├── ├── session
├── ├── └── templates
├── ├── _smoke_tests
├── ├── ├── _polars
├── ├── ├── ai
├── ├── ├── altair
├── ├── ├── anywidget_examples
├── ├── ├── anywidget_smoke_tests
├── ├── ├── appcomp
├── ├── ├── ├── double_nested
├── ├── ├── ├── imperative_output
├── ├── ├── └── state
├── ├── ├── charts
├── ├── ├── chat
├── ├── ├── compat
├── ├── ├── custom_server
├── ├── ├── docs
├── ├── ├── errors
├── ├── ├── inputs
├── ├── ├── issues
├── ├── ├── └── layouts
├── ├── ├── layouts
├── ├── ├── markdown
├── ├── ├── mpl
├── ├── ├── sandbox
├── ├── ├── sql
├── ├── ├── tables
├── ├── ├── theming
├── ├── └── third_party
├── ├── └── ├── leafmap
├── ├── └── └── pandas
├── ├── _snippets
├── ├── └── data
├── ├── _sql
├── ├── _tutorials
├── └── _utils
├── └── └── config
├── openapi
├── └── src
├── pyodide
├── scripts
└── tests
└── ├── _ai
└── ├── _ast
└── ├── ├── app_data
└── ├── ├── cell_data
└── ├── └── codegen_data
└── ├── _cli
└── ├── ├── cli_data
└── ├── ├── ipynb_data
└── ├── └── snapshots
└── ├── _config
└── ├── _convert
└── ├── _data
└── ├── └── snapshots
└── ├── _dependencies
└── ├── _islands
└── ├── └── snapshots
└── ├── _messaging
└── ├── _output
└── ├── └── formatters
└── ├── _plugins
└── ├── ├── core
└── ├── ├── stateless
└── ├── ├── └── status
└── ├── └── ui
└── ├── └── ├── _core
└── ├── └── └── _impl
└── ├── └── └── ├── charts
└── ├── └── └── ├── chat
└── ├── └── └── ├── dataframes
└── ├── └── └── ├── snapshots
└── ├── └── └── ├── tables
└── ├── └── └── ├── └── snapshots
└── ├── └── └── └── utils
└── ├── _runtime
└── ├── ├── layout
└── ├── ├── output
└── ├── ├── packages
└── ├── ├── reload
└── ├── ├── └── reload_data
└── ├── ├── runner
└── ├── ├── runtime_data
└── ├── ├── script_data
└── ├── └── snapshots
└── ├── _save
└── ├── _server
└── ├── ├── ai
└── ├── ├── └── snapshots
└── ├── ├── api
└── ├── ├── └── endpoints
└── ├── ├── export
└── ├── ├── └── snapshots
└── ├── ├── files
└── ├── ├── models
└── ├── ├── session
└── ├── └── templates
└── ├── └── ├── data
└── ├── └── └── snapshots
└── ├── _smoke_tests
└── ├── _snippets
└── ├── _sql
└── └── _utils
└── └── ├── config
└── └── └── snapshots
</file_tree>
<file_contents>
File: /Users/morganmcguire/ML/marimo/docs/_static/js/analytics.js
```js
// Check if URL ends with .html and redirect
if (window.location.pathname.endsWith('.html')) {
const redirectUrl = window.location.pathname.slice(0, -5) + window.location.search + window.location.hash;
console.log('Redirecting to', redirectUrl);
window.location.href = redirectUrl;
}
// @ts-ignore
// biome-ignore
!function(t,e){var o,n,p,r;e.__SV||(window.posthog=e,e._i=[],e.init=function(i,s,a){function g(t,e){var o=e.split(".");2==o.length&&(t=t[o[0]],e=o[1]),t[e]=function(){t.push([e].concat(Array.prototype.slice.call(arguments,0)))}}(p=t.createElement("script")).type="text/javascript",p.async=!0,p.src=s.api_host+"/static/array.js",(r=t.getElementsByTagName("script")[0]).parentNode.insertBefore(p,r);var u=e;for(void 0!==a?u=e[a]=[]:a="posthog",u.people=u.people||[],u.toString=function(t){var e="posthog";return"posthog"!==a&&(e+="."+a),t||(e+=" (stub)"),e},u.people.toString=function(){return u.toString(1)+".people (stub)"},o="capture identify alias people.set people.set_once set_config register register_once unregister opt_out_capturing has_opted_out_capturing opt_in_capturing reset isFeatureEnabled onFeatureFlags getFeatureFlag getFeatureFlagPayload reloadFeatureFlags group updateEarlyAccessFeatureEnrollment getEarlyAccessFeatures getActiveMatchingSurveys getSurveys".split(" "),n=0;n<o.length;n++)g(u,o[n]);e._i.push([i,s,a])},e.__SV=1)}(document,window.posthog||[]);
// @ts-ignore
posthog.init("phc_U2xS6waxdmM6YDt9l6jwDdluVinawgkSn69qPAohz59", {
api_host: "https://app.posthog.com",
});
```
File: /Users/morganmcguire/ML/marimo/docs/_static/js/math.js
```js
(() => {
const renderMath = (element) => {
const tex = element.textContent || element.innerHTML;
if (tex.startsWith("\\(") && tex.endsWith("\\)")) {
katex.render(tex.slice(2, -2), element, { displayMode: false });
} else if (tex.startsWith("\\[") && tex.endsWith("\\]")) {
katex.render(tex.slice(2, -2), element, { displayMode: true });
}
};
const renderAllMath = () => {
const maths = document.querySelectorAll(
".arithmatex:not([data-processed])",
);
maths.forEach((element) => {
try {
renderMath(element);
element.setAttribute("data-processed", "true");
} catch (error) {
console.warn("Failed to render math:", error);
}
});
};
// Watch for new content
const observer = new MutationObserver((mutations) => {
const shouldRender = mutations.some((mutation) => {
return mutation.addedNodes.length > 0;
});
if (shouldRender) {
renderAllMath();
}
});
const init = () => {
if (typeof katex === "undefined") {
console.warn("KaTeX not loaded");
return;
}
renderAllMath();
observer.observe(document.body, {
childList: true,
subtree: true,
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}
})();
```
File: /Users/morganmcguire/ML/marimo/docs/_static/motherduck/motherduck_dag.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/motherduck/motherduck_db_discovery.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/motherduck/motherduck_python_and_sql.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/motherduck/motherduck_reactivity.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/motherduck/motherduck_sql.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/array.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/ai-completion.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/CalSans-SemiBold.woff
```woff
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-ai-completion-codeium-vscode-dark.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-ai-completion-codeium-vscode.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-ai-completion-codeium-vscode-download-diagnostics.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-app-config.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-cell-actions.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-output.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-table.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-command-palette.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-transform-code.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-table.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-ai-completion-custom-assist-rules.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-transform.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-transform.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-table.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-ai-completion-codeium-vscode-download-diagnostics-dark.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-transform.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-delete-cell.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-df.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-delete-cell.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-delete-cell.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-df.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-df.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-disable-cell.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dataframe-visualizations.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-dependency-graph.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-enable-cell.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-disable-cell.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-disable-cell.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-enable-cell.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-html-export.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-enable-cell.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-feedback-form.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro-app.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro-app.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro-app.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-markdown-toggle.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-intro.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-markdown-toggle.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-model-comparison.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-model-comparison.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-model-comparison.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-markdown-toggle.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-module-reloading-lazy.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-module-reloading.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-multi-column.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-runtime-config.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-sql-df.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-sql-cell.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-panel-icons.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-settings.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-sql-http.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-sql-engine-dropdown.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-counter.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-task-list.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-tied.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-counter.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-counter.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-task-list.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-task-list.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-tied.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-user-config.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-state-tied.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/embedding.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/docs-user-config.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/embedding.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/embedding.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/faq-marimo-ui.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/favicon-32x32.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/intro_condensed.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/faq-marimo-ui.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/faq-marimo-ui.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/intro_condensed.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/outputs.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/marimo-logotype-horizontal.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/marimo-logotype-thick.svg
```svg
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="185.53143"
height="148.3338"
viewBox="0 0 139.14857 111.25034"
version="1.1"
id="svg1568"
sodipodi:docname="marimo-logotype-thick.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1572" />
<sodipodi:namedview
id="namedview1570"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="pt"
showgrid="false"
inkscape:zoom="1.6749344"
inkscape:cx="-180.30557"
inkscape:cy="-47.166028"
inkscape:window-width="2560"
inkscape:window-height="1403"
inkscape:window-x="2560"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1568" />
<g
id="surface1"
transform="matrix(0.32633463,0,0,0.32633463,-126.64588,-120.45361)">
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 388.08594,708.44141 h 23.32422 v -33.30079 c 0,-4.83203 2.625,-7.03906 6.30078,-7.03906 3.88672,0 6.19922,2.20703 6.19922,7.03906 v 33.30079 h 23.32031 v -33.30079 c 0,-4.83203 2.51953,-7.03906 6.30469,-7.03906 3.99218,0 6.30078,2.20703 6.30078,7.03906 v 33.30079 h 23.42578 V 670.625 c 0,-17.33594 -9.13672,-23.11328 -20.48438,-23.11328 -9.875,0 -15.96875,5.67578 -17.4375,10.92578 H 444.5 c -3.57422,-8.19141 -8.82422,-10.92578 -16.59766,-10.92578 -8.30078,0 -14.18359,4.9375 -15.65234,9.98047 h -0.83984 v -8.40235 h -23.32422 z m 102.53125,-29.625 c 0,18.28125 10.82031,31.20312 26.26172,31.20312 7.98437,0 14.18359,-3.46875 16.80859,-9.98047 h 0.73437 v 8.40235 h 23.11329 v -59.35157 h -23.11329 v 8.40235 h -0.73437 c -2.625,-6.40625 -8.82422,-9.98047 -16.80859,-9.98047 -14.4961,0 -26.26172,12.29297 -26.26172,31.30469 z m 33.71875,10.61328 c -5.77735,0 -10.29297,-3.67969 -10.29297,-10.71485 0,-6.82812 4.41015,-10.61328 10.29297,-10.61328 5.67578,0 10.1914,3.67969 10.1914,10.61328 0,7.03516 -4.51562,10.71485 -10.1914,10.71485 z m 84.04297,-15.02344 v -26.89453 c -9.2461,0 -15.65235,4.9375 -18.80469,14.70703 h -0.83985 v -13.12891 h -21.11718 v 59.35157 H 590.625 v -21.42969 c 0,-8.71875 4.83203,-13.44531 11.87109,-13.44531 2.625,0 4.6211,0.41796 5.88282,0.83984 z m 7.14062,34.03516 h 23.21875 v -59.35157 h -23.21875 z m 0,-65.55079 h 23.21875 v -6.40625 h -23.21875 z m 33.30078,65.55079 h 23.32422 v -33.30079 c 0,-4.83203 2.625,-7.03906 6.30078,-7.03906 3.88672,0 6.19922,2.20703 6.19922,7.03906 v 33.30079 h 23.32031 v -33.30079 c 0,-4.83203 2.52344,-7.03906 6.30469,-7.03906 3.99219,0 6.30078,2.20703 6.30078,7.03906 v 33.30079 h 23.42578 V 670.625 c 0,-17.33594 -9.13672,-23.11328 -20.48437,-23.11328 -9.875,0 -15.96485,5.67578 -17.4375,10.92578 h -0.83985 c -3.57031,-8.19141 -8.82421,-10.92578 -16.59765,-10.92578 -8.30078,0 -14.1836,4.9375 -15.65235,9.98047 h -0.83984 v -8.40235 h -23.32422 z m 134.15235,1.57812 c 18.27734,0 31.51171,-13.02734 31.51171,-31.30469 0,-18.80468 -13.23437,-31.20312 -31.51171,-31.20312 -18.38672,0 -31.6211,12.39844 -31.6211,31.20312 0,18.27735 11.13672,31.30469 31.6211,31.30469 z m 0,-20.58984 c -5.77735,0 -10.29688,-3.67969 -10.29688,-10.71485 0,-6.82812 4.41406,-10.61328 10.29688,-10.61328 5.67187,0 10.1875,3.67969 10.1875,10.61328 0,7.03516 -4.51563,10.71485 -10.1875,10.71485 z m 0,0"
id="path1531" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 517.37109,413.55859 c 1.51172,-1.78125 3.1875,-4.15625 2.10547,-6.26172 6.04688,-3.5625 11.39063,-8.26171 15.76172,-13.76562 -2.375,1.89062 -4.96484,3.40234 -7.71875,4.58984 20.67578,-20.40625 51.76953,-29.6914 80.32422,-23.96875 -22.99609,-0.86328 -46.42188,5.23438 -65.42578,18.19141 -19.05469,12.90234 -33.51953,32.71094 -39.07813,55.05859 8.41797,-24.45312 26.17969,-45.55859 48.85157,-57.97265 -1.78125,1.83593 -3.56641,3.72265 -5.39844,5.55859 8.63672,-3.5625 16.46094,-8.90625 25.04687,-12.63281 25.20703,-11.01172 55.54297,-6.85547 79.02344,7.5039 23.48047,14.35938 40.37891,37.89454 50.20313,63.58985 4.3164,11.22656 7.33984,23.69531 3.50781,35.08594 -0.86328,-14.84375 -5.72266,-29.14844 -12.03906,-42.58985 -10.47266,-22.40234 -25.47657,-43.39844 -46.58204,-56.24609 -21.10546,-12.79297 -49.01562,-16.19531 -70.60546,-4.15625 11.0664,-1.1875 22.23828,-1.83594 33.19531,-0.26953 20.62109,2.91406 39.67578,13.71093 54.14062,28.66015 14.41407,15.00782 24.40235,33.95313 30.28516,53.8711 5.72266,19.48828 7.60937,40.97265 0.26953,59.97265 21.42969,-34.65625 17.86719,-82.91015 -8.3125,-114.05859 -10.95703,-13.00781 -25.15625,-22.99609 -40.48437,-30.22656 -17.70704,-8.3125 -37.14063,-13.11719 -56.67969,-12.57813 -19.53906,0.53906 -39.08203,6.64063 -54.67969,18.40625 12.95313,-11.44531 29.84766,-17.92187 47.01563,-19.70312 17.21875,-1.72657 34.65234,1.08203 51.01171,6.58593 34.00391,11.55079 64.98829,37.03125 74.97657,71.52344 2.53515,8.85156 3.66797,18.02735 4.69531,27.20703 0.86328,7.9336 1.56641,15.86719 1.40234,23.85547 -0.7539,39.46094 -25.96484,78.00391 -62.72265,92.41406 -10.41797,4.05079 -21.42969,6.26172 -32.4961,7.82813 -17.70703,2.48047 -36.05859,3.23828 -53.11718,-2.05078 -8.79688,-2.75391 -17.00391,-7.01953 -24.61329,-12.25391 -31.52343,-21.59375 -51.4414,-59.21484 -51.4414,-97.43359 0,-6.80078 0.59375,-13.54688 1.51172,-20.24219 0.80859,-6.15234 1.88672,-12.25391 3.66797,-18.19141 2.69921,-8.74609 6.91015,-17.0039 12.47265,-24.28906 0.96875,-0.59375 1.94141,-1.24219 2.85938,-1.89062 -12.79297,27.04297 -20.40625,59 -8.69141,86.58203 -4.21094,-17.97266 -4.48047,-36.8125 -0.75391,-54.84375 1.1875,-0.42969 2.375,-0.91406 3.50782,-1.40235 0.0547,-1.1875 0.10547,-2.32031 0.16015,-3.50781 -0.86328,0.97266 -1.67187,1.94141 -2.53515,2.85938 1.99609,-14.30469 8.14843,-27.85157 17.3789,-38.8086 m 8.69141,-0.70312 c 3.34766,-2.26563 6.69531,-4.53516 10.04297,-6.80078 -2.10547,4.26562 -5.99219,7.60937 -10.52735,9.12109 -21.05078,21.75391 -27.53125,54.19531 -25.04687,84.31641 0.97266,11.39062 3.29297,23.21093 10.63281,31.95703 -5.125,-21.05469 -5.39844,-43.29297 -0.69922,-64.45313 0.91797,-0.0547 1.23828,1.13282 1.23828,2.05078 0.37891,14.19922 -2.05078,28.39454 -1.02343,42.58985 1.02343,14.14453 6.3164,29.09765 18.1914,36.8164 9.28516,6.04297 18.51563,12.08985 27.7461,18.13672 -21.59375,-6.91015 -40.4336,-21.91797 -52.08985,-41.34765 3.12891,8.47265 7.5,16.51562 12.89844,23.75 -0.69922,-1.94532 -1.34766,-3.94141 -2.05078,-5.9375 5.45312,5.1289 10.95703,10.14843 16.41016,15.22265 3.99609,3.72266 10.25781,7.5 14.35937,3.9375 0.64844,1.35157 -0.53906,3.02344 -1.99609,3.40235 -1.46094,0.43359 -3.02344,0 -4.48047,-0.4336 6.53125,4.69922 13.33203,9.01563 20.34765,12.95703 -1.02343,0.16016 -1.34765,1.67188 -0.69921,2.53516 0.64453,0.86719 1.72656,1.08203 2.75,1.35156 25.75,5.5586 53.17187,3.50782 77.78515,-5.99218 -21.16015,4.96484 -43.72265,6.10156 -64.23437,-1.07813 -17.32813,-6.04687 -32.4961,-17.97656 -42.42969,-33.35937 -3.18359,-4.96875 -5.82813,-10.20313 -8.25781,-15.54688 -7.82813,-17.59766 -12.79297,-36.8125 -11.39063,-56.03125 1.40625,-19.21484 9.82422,-38.37891 24.94141,-50.25391 1.5625,-1.24218 3.50781,-2.42968 5.44922,-1.94531 -14.89844,10.79688 -23.75,28.66406 -26.07032,46.91016 -2.32031,18.29687 1.45704,36.86719 8.09766,54.03125 4.75,12.14844 11.0625,23.91406 19.75391,33.6289 3.72656,4.15625 7.9375,7.9375 12.73828,10.79688 8.80078,5.28906 19.21875,7.28906 29.42187,8.58203 13.8711,1.78125 27.85157,2.53906 41.83203,2.21484 -17.65234,4.15625 -36.11328,1.1875 -53.98046,-1.78125 30.33984,11.39063 65.91015,4.42579 91.98437,-14.73828 5.17969,-3.83203 10.09375,-8.09375 14.03125,-13.22265 6.04688,-7.88282 9.66406,-17.48828 11.5,-27.26172 4.75,-25.47656 -2.05078,-52.08985 -14.41406,-74.92188 -8.58203,-15.87109 -19.97266,-30.55078 -34.97656,-40.53906 -14.03516,-9.28516 -30.71485,-14.19531 -47.50391,-15.11328 -25.80078,-1.51172 -53.11328,7.01562 -70.28125,26.44922 M 576.75,588.17969 c 11.60547,2.91406 23.32031,5.01953 35.19531,6.42578 -14.57422,3.34375 -29.58203,-1.78125 -43.66797,-6.80078 -8.3164,-2.91797 -16.73437,-5.94141 -24.45312,-10.25782 -18.67969,-10.36328 -32.71485,-28.125 -40.86328,-47.8789 -1.40235,-3.50781 -2.69922,-7.01953 -3.77735,-10.58203 -0.10937,-0.42969 -0.43359,0.59375 0,0.3789 0.42969,-0.16015 0.375,-0.80859 0.16016,-1.24218 -4.85938,-11.71094 -7.82813,-24.28907 -8.74609,-36.92188 -0.91797,19.10938 3.83203,38.32422 12.46875,55.4375 9.39453,18.62109 23.48437,34.98047 41.1875,46.04297 17.65234,11.06641 38.91796,16.67969 59.64843,14.68359 29.14844,-2.85937 54.73438,-19.86328 78.21485,-37.30078 -10.36328,13.71094 -26.07032,22.1875 -41.34766,30.17578 26.39453,-7.39453 47.55469,-28.28515 60.29688,-52.57422 -6.31641,10.79297 -16.19532,19.26954 -26.9375,25.63672 -10.79688,6.3711 -22.61719,10.79688 -34.4375,15.0625 -5.1836,1.89063 -10.41797,3.72266 -15.76172,5.23438 -15.27735,4.375 -31.36328,5.88672 -47.17969,4.48047 m 107.95703,-36.48828 c 2.69922,-2.53907 5.34766,-5.12891 7.50391,-8.04297 3.40234,-4.69532 5.66797,-10.14844 7.82812,-15.54688 2.91406,-7.33984 5.88281,-14.73828 6.64063,-22.61719 -1.40625,1.24219 -1.89063,3.07813 -2.42969,4.85938 -7.17969,24.88281 -24.9375,46.58203 -47.98828,58.45703 10.85156,-2.42969 20.1875,-9.5 28.44531,-17.10937 m -131.92187,26.44921 c 1.13281,0.26954 2.53515,0.75391 2.58984,1.89063 0.42969,-0.75781 1.72656,-0.75781 2.21094,0 0.21875,-0.0547 0.0547,-0.43359 -0.21485,-0.54297 -1.61718,-0.86328 -3.23828,-1.72656 -4.80468,-2.58984 -6.53125,-3.50782 -12.52344,-7.87891 -17.86719,-12.95313 -0.64844,1.40235 -0.16016,3.1836 1.08203,4.04688 2.375,1.72656 4.75,3.45312 7.125,5.18359 2.85937,2.375 6.26172,4.10156 9.87891,4.96484 m 155.89062,-81.61718 c -0.21484,3.99609 -0.43359,8.04297 -0.70312,12.03906 -0.0508,0.53906 -0.0508,1.07812 -0.0508,1.61719 -0.0547,0.97265 -0.10937,1.94531 -0.21875,2.86328 -0.0547,1.1875 -0.10546,2.26562 -0.10546,3.45312 2.26562,-9.17578 3.07421,-18.78515 2.375,-28.17578 -0.26954,-0.10937 -0.54297,-0.26953 -0.75782,-0.43359 -0.16015,2.85937 -0.3789,5.72265 -0.53906,8.63672 m -209.8164,-59.16016 c -0.3789,0.69922 -0.70312,1.40234 -1.02734,2.10547 0.37891,0.48437 1.07813,-0.10938 1.24219,-0.70313 0.16015,-0.53906 0.32422,-1.29687 0.91797,-1.34765 -0.26953,-0.27344 -0.53907,-0.54297 -0.8086,-0.8125 -0.10937,0.21875 -0.21875,0.53906 -0.10937,0.75781 m 39.02734,-44.80469 c 0.53906,-0.42968 1.07813,-0.86328 1.61719,-1.34765 -0.91797,-0.37891 -2.10547,0.10547 -2.48047,1.07812 0.26953,0.10938 0.53906,0.21485 0.86328,0.26953 m 1.67188,-2.53515 c 1.1875,0.26953 2.53906,-0.0547 3.50781,-0.86328 -1.23828,-0.0547 -2.48047,0.21484 -3.50781,0.86328 m -42.96485,54.78906 c -0.0547,-0.26953 -0.10937,-0.53906 -0.10937,-0.75781 -1.02735,0.3789 -1.51172,1.78125 -0.91797,2.69922 0.21484,-0.69922 0.53906,-1.34766 1.02734,-1.94141 m -6.10156,27.52734 c -0.26953,0.37891 -0.0547,1.02735 0.43359,1.08203 0.26954,-0.86328 0.21485,-1.78125 -0.0547,-2.69921 -0.53907,0.26953 -0.70313,1.02343 -0.37891,1.51171"
id="path1533" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 527.08984,398.39062 c 0.10547,0.16016 0.21485,0.375 0.32422,0.53907 -0.48828,0.69922 -1.40625,1.02343 -2.21484,0.80859 0.43359,-0.59375 1.08203,-1.02344 1.78125,-1.1875"
id="path1535" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 522.82422,401.84375 c 0.75781,-0.26953 1.5664,-0.43359 2.375,-0.37891 -0.16016,1.1875 -2.16016,1.51172 -2.64453,0.37891"
id="path1537" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 520.125,399.35937 c 0.48828,-0.7539 1.40234,-1.29296 2.375,-1.1875 0.21484,0.54297 0.0547,1.1875 -0.37891,1.56641 -0.7539,0.10938 -1.67187,0.21484 -2.21093,-0.37891"
id="path1539" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 671.42969,402.92187 c 0.32422,-0.0508 0.64843,0.26954 0.59375,0.59375 -0.0547,0.32422 -0.42969,0.54297 -0.75391,0.4336 -0.48828,-0.53906 -0.97266,-1.13281 -1.45703,-1.72656 0.53906,-0.4336 1.50781,-0.0547 1.61719,0.59375"
id="path1541" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 524.98437,394.93359 c 0.37891,-0.42968 1.13282,-0.42968 1.56641,-0.0547 -0.70312,0.70312 -1.62109,1.1875 -2.59375,1.51171 -0.10547,-0.59375 0.43359,-1.29296 1.02734,-1.24218"
id="path1543" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 516.13281,445.13672 c 0.48438,-0.70313 1.78125,-0.4336 1.94141,0.42969 -0.64844,-0.10547 -1.34766,0.10937 -1.78125,0.59375 -0.26953,0.10937 -0.70313,-0.10547 -0.70313,-0.42969 -0.0508,-0.26953 0.27344,-0.59375 0.59375,-0.54297"
id="path1545" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 547.00781,410.58984 c 0.26953,-0.10937 0.64844,-0.21875 0.91797,-0.10937 0.32422,0.10937 0.48438,0.59375 0.26953,0.80859 -1.02734,0.64844 -2.21484,1.02735 -3.40234,1.08203 0.54297,-0.75781 1.35156,-1.35156 2.21484,-1.78125"
id="path1547" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 562.92969,579.27344 c 0.54297,0.3789 1.13672,0.75781 1.67578,1.13281 -1.1875,0.48828 -2.58985,0.21875 -3.5625,-0.64453 -0.21485,-0.37891 0.21484,-0.91797 0.59375,-0.91797 0.48437,-0.0547 0.91797,0.21484 1.29297,0.42969"
id="path1549" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 538.26172,405.62109 c -0.26953,-0.80859 -0.0547,-1.78125 0.59375,-2.375 0.43359,-0.10547 0.86328,-0.42968 1.02734,-0.91797 0.69922,0.26954 1.34766,0.54297 2.05078,0.8125 -1.1875,0.8086 -2.42968,1.61719 -3.67187,2.48047"
id="path1551" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 511.92187,463.59766 c 0,-0.32422 0,-0.64844 0,-0.97266 0.53907,-0.26953 1.13282,-0.26953 1.61719,-0.0547 -0.96875,1.02735 -1.51172,2.42969 -1.40234,3.83203 -0.59375,-0.42968 -1.24219,-0.91797 -1.89063,-1.34765 0.54297,-0.4336 1.13672,-0.91797 1.73047,-1.40625"
id="path1553" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 508.35937,415.93359 c 0.21485,-1.67578 1.1875,-3.1875 2.53516,-4.21093 0.37891,-0.26954 0.97266,-0.4336 1.1875,-0.0547 -1.1875,1.1875 -2.16016,2.53515 -2.85937,4.04687 -0.16407,0.27344 -0.59375,0.4336 -0.86329,0.21875"
id="path1555" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 513.91797,449.39844 c 1.07812,-0.8086 2.10547,-1.61719 3.18359,-2.42969 -0.26953,1.02734 -0.53906,1.94531 -0.80859,2.96875 -0.37891,0.21875 -0.64844,0.59766 -1.02344,0.8125 -0.37891,0.21484 -0.91797,0.26953 -1.1875,-0.10938 -0.21875,-0.3789 0.375,-0.91796 0.59375,-0.53906 -0.21875,-0.21484 -0.48828,-0.43359 -0.75781,-0.70312"
id="path1557" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 555.05078,407.56641 c -0.10937,0.26953 -0.21484,0.53906 -0.10937,0.86328 -0.86329,0.16015 -1.67188,0.32422 -2.42969,0.48437 -0.10547,0.32422 -0.21485,0.64844 -0.32031,0.97266 -0.64844,-0.0547 -1.29688,-0.0547 -1.89063,-0.0547 -0.53906,-0.26953 0,-1.02344 0.53906,-1.29297 1.07813,-0.54297 2.21485,-0.86719 3.40235,-1.02734 -0.16407,-0.26953 -0.32422,-0.48438 -0.48828,-0.75391 0.86328,-0.70312 1.89062,-1.1875 3.02343,-1.51172 0.21485,0.375 0.26953,0.8086 0.16407,1.23828 -0.37891,0.16407 -0.75782,0.21875 -1.13672,0.10938 0.0547,0.26953 0.10937,0.53906 0.16406,0.86328 -0.37891,0.10938 -0.64844,0.16406 -0.91797,0.10938"
id="path1559" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 512.83984,412.64062 c 0.42969,-0.26953 1.02344,-0.3789 1.51172,-0.26953 -0.21875,-0.26953 -0.3789,-0.54297 -0.54297,-0.8125 0.54297,-0.10547 1.08203,-0.21484 1.67578,-0.26953 0.21485,-0.53906 0.21485,-1.1875 0,-1.72656 0.59375,-0.0547 1.24219,0.0547 1.78125,0.26953 0.16016,0.21484 0.21485,0.53906 0.16016,0.80859 -0.26953,0.10938 -0.59375,0.21875 -0.91797,0.32422 -0.10547,0.54297 -0.26953,1.08203 -0.375,1.6211 -0.26953,0.16015 -0.70312,0.21484 -1.02734,0.10937 -0.48438,2.91406 -3.67188,4.58594 -4.80469,7.28516 -0.37891,0.91797 -0.75391,2.10547 -1.72656,2.10547 -0.26953,-0.32422 -0.21485,-0.91797 0.16015,-1.1875 -0.375,-0.10938 -0.69921,-0.21485 -1.02343,-0.32422 -0.0547,-0.59375 0.91797,-0.42969 1.40234,-0.0547 0.43359,-0.375 0.0547,-1.24219 -0.48437,-1.24219 0.59375,-0.48437 0.97265,-1.23828 1.07812,-1.99609 0.59375,0.0547 1.1875,-0.16016 1.56641,-0.59375 -0.4336,0.10937 -0.91797,-0.21484 -1.02735,-0.69922 0.86328,0.16016 1.73047,-0.64844 1.67578,-1.51172 0.64844,-0.3789 1.29297,-0.75781 1.94141,-1.13672 -0.26953,-0.16015 -0.64844,-0.42968 -1.02344,-0.69922"
id="path1561" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 682.38672,415.5 c -1.1875,-1.61719 -2.42969,-3.18359 -3.9375,-4.53516 -2.10547,-1.88672 -4.80469,-3.5625 -5.23828,-6.36718 12.6875,10.74218 22.45703,24.9375 27.90625,40.64453 0.48828,0.43359 -0.0508,1.40625 -0.64453,1.24218 -4.75,-10.95703 -10.85157,-21.42968 -18.08594,-30.98437"
id="path1563" />
<path
style="fill:#1c7361;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m 539.88281,406.97266 c 2.21094,-1.78125 4.75,-3.45704 5.66797,-6.15625 0.26953,-0.75391 1.02344,-1.34766 1.72656,-1.78125 10.90235,-6.85547 23.64453,-10.63282 36.48828,-10.84766 -6.42187,1.72656 -12.79296,3.39844 -19.21484,5.12891 -9.98437,2.64453 -17.32812,10.90234 -24.07422,18.78515 -5.83203,6.85547 -11.71484,13.71094 -17.54297,20.61719 -0.70312,1.89062 -1.35156,3.83203 -2.05078,5.72266 -0.26953,0.86328 -0.59375,1.72656 -1.35156,2.26562 -0.69922,0.54297 -1.88672,0.54297 -2.32031,-0.26953 2.58984,-4.53125 5.18359,-9.12109 7.82812,-13.71094 4.21094,-7.125 8.41797,-14.46484 14.84375,-19.7539"
id="path1565" />
</g>
</svg>
```
File: /Users/morganmcguire/ML/marimo/docs/_static/outputs.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/reactive.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/reactive.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/outputs.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-sql-cell.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/reactive.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui-form.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui-form.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui-form.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/readme-ui.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/share-wasm-link.webm
```webm
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/array.md
```md
# Array
/// marimo-embed
```python
@app.cell
def __():
wish = mo.ui.text(placeholder="Wish")
wishes = mo.ui.array([wish] * 3, label="Three wishes")
return
@app.cell
def __():
mo.hstack([wishes, wishes.value], justify="space-between")
return
```
///
::: marimo.ui.array
```
File: /Users/morganmcguire/ML/marimo/docs/_static/share-wasm-link.gif
```gif
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/_static/share-wasm-link.mp4
```mp4
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/batch.md
```md
# Batch
/// marimo-embed
```python
@app.cell
def __():
el = mo.md("{start} → {end}").batch(
start=mo.ui.date(label="Start Date"),
end=mo.ui.date(label="End Date")
)
el
return
@app.cell
def __():
el.value
return
```
///
::: marimo.ui.batch
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/anywidget.md
```md
# Building custom UI elements
Build custom UI plugins that hook into marimo's reactive
execution engine by using [anywidget](https://anywidget.dev/).
[anywidget](https://anywidget.dev/) is a Python library and specification for
creating custom Jupyter-compatible widgets. marimo supports anywidget, allowing
you to import anywidget widgets or create your own custom widgets and use them
in your notebooks and apps.
## Importing a widget
You can use anywidgets that others have built, such as
[quak](https://github.com/manzt/quak) or
[drawdata](https://github.com/koaning/drawdata), directly in marimo.
Here is an example using `drawdata`:
```python
# pip install drawdata
from drawdata import ScatterWidget
widget = mo.ui.anywidget(ScatterWidget())
# In another cell, you can access the widget's value
widget.value
# You can also access the widget's specific properties
widget.data
widget.data_as_polars
```
For additional examples, see
[our repo](https://github.com/marimo-team/marimo/tree/main/examples/third_party/anywidget).
## Custom widget
```python
import anywidget
import traitlets
import marimo as mo
class CounterWidget(anywidget.AnyWidget):
# Widget front-end JavaScript code
_esm = """
function render({ model, el }) {
let getCount = () => model.get("count");
let button = document.createElement("button");
button.innerHTML = `count is ${getCount()}`;
button.addEventListener("click", () => {
model.set("count", getCount() + 1);
model.save_changes();
});
model.on("change:count", () => {
button.innerHTML = `count is ${getCount()}`;
});
el.appendChild(button);
}
export default { render };
"""
_css = """
button {
padding: 5px !important;
border-radius: 5px !important;
background-color: #f0f0f0 !important;
&:hover {
background-color: lightblue !important;
color: white !important;
}
}
"""
# Stateful property that can be accessed by JavaScript & Python
count = traitlets.Int(0).tag(sync=True)
widget = mo.ui.anywidget(CounterWidget())
# In another cell, you can access the widget's value
widget.value
# You can also access the widget's specific properties
widget.count
```
---
::: marimo.ui.anywidget
```
File: /Users/morganmcguire/ML/marimo/docs/_static/vscode-marimo.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/button.md
```md
# Button
!!! tip "Looking for a submit/run button?"
If you're looking for a button to trigger computation on click, consider
using [`mo.ui.run_button`][marimo.ui.run_button].
/// marimo-embed-file
filepath: examples/ui/button.py
///
::: marimo.ui.button
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/code_editor.md
```md
# Code Editor
/// marimo-embed-file
filepath: examples/ui/code_editor.py
///
::: marimo.ui.code_editor
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/chat.md
```md
# Chat
!!! tip "Looking for example notebooks?"
For example notebooks, check out [`examples/ai/chat` on our
GitHub](https://github.com/marimo-team/marimo/tree/main/examples/ai/chat).
/// marimo-embed
size: large
```python
@app.cell
def __():
def simple_echo_model(messages, config):
return f"You said: {messages[-1].content}"
mo.ui.chat(
simple_echo_model,
prompts=["Hello", "How are you?"],
show_configuration_controls=True
)
return
```
///
The chat UI element provides an interactive chatbot interface for
conversations. It can be customized with different models, including built-in
AI models from popular providers or custom functions.
::: marimo.ui.chat
## Basic Usage
Here's a simple example using a custom echo model:
```python
import marimo as mo
def echo_model(messages, config):
return f"Echo: {messages[-1].content}"
chat = mo.ui.chat(echo_model, prompts=["Hello", "How are you?"])
chat
```
Here, `messages` is a list of [`ChatMessage`][marimo.ai.ChatMessage] objects,
which has `role` (`"user"`, `"assistant"`, or `"system"`) and `content` (the
message string) attributes; `config` is a
[`ChatModelConfig`][marimo.ai.ChatModelConfig] object with various
configuration parameters, which you are free to ignore.
## Using a Built-in AI Model
You can use marimo's built-in AI models, such as OpenAI's GPT:
```python
import marimo as mo
chat = mo.ui.chat(
mo.ai.llm.openai(
"gpt-4",
system_message="You are a helpful assistant.",
),
show_configuration_controls=True
)
chat
```
## Accessing Chat History
You can access the chat history using the `value` attribute:
```python
chat.value
```
This returns a list of [`ChatMessage`][marimo.ai.ChatMessage] objects, each
containing `role`, `content`, and optional `attachments` attributes.
::: marimo.ai.ChatMessage
## Custom Model with Additional Context
Here's an example of a custom model that uses additional context:
```python
import marimo as mo
def rag_model(messages, config):
question = messages[-1].content
docs = find_relevant_docs(question)
context = "\n".join(docs)
prompt = f"Context: {context}\n\nQuestion: {question}\n\nAnswer:"
response = query_llm(prompt, config)
return response
mo.ui.chat(rag_model)
```
This example demonstrates how you can implement a Retrieval-Augmented
Generation (RAG) model within the chat interface.
## Templated Prompts
You can pass sample prompts to `mo.ui.chat` to allow users to select from a
list of predefined prompts. By including a `{{var}}` in the prompt, you can
dynamically insert values into the prompt; a form will be generated to allow
users to fill in the variables.
```python
mo.ui.chat(
mo.ai.llm.openai("gpt-4o"),
prompts=[
"What is the capital of France?",
"What is the capital of Germany?",
"What is the capital of {{country}}?",
],
)
```
## Including Attachments
You can allow users to upload attachments to their messages by passing an
`allow_attachments` parameter to `mo.ui.chat`.
```python
mo.ui.chat(
rag_model,
allow_attachments=["image/png", "image/jpeg"],
# or True for any attachment type
# allow_attachments=True,
)
```
## Built-in Models
marimo provides several built-in AI models that you can use with the chat UI
element.
### OpenAI
```python
import marimo as mo
mo.ui.chat(
mo.ai.llm.openai(
"gpt-4o",
system_message="You are a helpful assistant.",
api_key="sk-proj-...",
),
show_configuration_controls=True
)
```
::: marimo.ai.llm.openai
### Anthropic
```python
import marimo as mo
mo.ui.chat(
mo.ai.llm.anthropic(
"claude-3-5-sonnet-20240620",
system_message="You are a helpful assistant.",
api_key="sk-ant-...",
),
show_configuration_controls=True
)
```
::: marimo.ai.llm.anthropic
### Google AI
```python
import marimo as mo
mo.ui.chat(
mo.ai.llm.google(
"gemini-1.5-pro-latest",
system_message="You are a helpful assistant.",
api_key="AI..",
),
show_configuration_controls=True
)
```
::: marimo.ai.llm.google
### Groq
```python
import marimo as mo
mo.ui.chat(
mo.ai.llm.groq(
"llama-3.1-70b-versatile",
system_message="You are a helpful assistant.",
api_key="gsk-...",
),
show_configuration_controls=True
)
```
::: marimo.ai.llm.groq
## Types
Chatbots can be implemented with a function that receives a list of
[`ChatMessage`][marimo.ai.ChatMessage] objects and a
[`ChatModelConfig`][marimo.ai.ChatModelConfig].
::: marimo.ai.ChatMessage
::: marimo.ai.ChatModelConfig
[`mo.ui.chat`][marimo.ui.chat] can be instantiated with an initial
configuration with a dictionary conforming to the config.
`ChatMessage`s can also include attachments.
::: marimo.ai.ChatAttachment
## Supported Model Providers
We support any OpenAI-compatible endpoint. If you want any specific provider added explicitly (ones that don't abide by the standard OpenAI API format), you can file a [feature request](https://github.com/marimo-team/marimo/issues/new?template=feature_request.yaml).
Normally, overriding the `base_url` parameter should work. Here are some examples:
/// tab | Cerebras
```python
chatbot = mo.ui.chat(
mo.ai.llm.openai(
model="llama3.1-8b",
api_key="csk-...", # insert your key here
base_url="https://api.cerebras.ai/v1/",
),
)
chatbot
```
///
/// tab | Groq
```python
chatbot = mo.ui.chat(
mo.ai.llm.openai(
model="llama-3.1-70b-versatile",
api_key="gsk_...", # insert your key here
base_url="https://api.groq.com/openai/v1/",
),
)
chatbot
```
///
/// tab | xAI
```python
chatbot = mo.ui.chat(
mo.ai.llm.openai(
model="grok-beta",
api_key=key, # insert your key here
base_url="https://api.x.ai/v1",
),
)
chatbot
```
///
!!! note
We have added examples for GROQ and Cerebras. These providers offer free API keys and are great for trying out Llama models (from Meta). You can sign up on their platforms and integrate with various AI integrations in marimo easily. For more information, refer to the [AI completion documentation in marimo](../../guides/editor_features/ai_completion.md).
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/checkbox.md
```md
# Checkbox
/// marimo-embed
```python
@app.cell
def __():
checkbox = mo.ui.checkbox(label="check me")
return
@app.cell
def __():
mo.hstack([checkbox, mo.md(f"Has value: {checkbox.value}")])
return
```
///
::: marimo.ui.checkbox
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/data_explorer.md
```md
# Data Explorer
The data explorer UI element outputs a visual editor explore your data via plotting and intelligent recommendations. You can incrementally build your "main" plot by adding different encodings: x-axis, y-axis, color, size, and shape. As you build your plot, the UI element will suggest further plots by intelligently "exploding" an additional encoding derived from your base plot.
!!! note "Pandas Required"
In order to use the dataframe UI element, you must have the `pandas` package installed.
You can install it with `pip install pandas`.
/// marimo-embed
size: large
app_width: full
```python
@app.cell
def __():
import pandas as pd
import pyodide
csv = pyodide.http.open_url("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv")
df = pd.read_csv(csv)
mo.ui.data_explorer(df)
return
```
///
::: marimo.ui.data_explorer
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/dates.md
```md
# Dates
## Single date
/// marimo-embed
```python
@app.cell
def __():
date = mo.ui.date(label="Start Date")
return
@app.cell
def __():
mo.hstack([date, mo.md(f"Has value: {date.value}")])
return
```
///
::: marimo.ui.date
:members:
## Date and time
```python
@app.cell
def __():
datetime = mo.ui.datetime(label="Start Date")
return
@app.cell
def __():
mo.hstack([datetime, mo.md(f"Has value: {datetime.value}")])
return
```
::: marimo.ui.datetime
:members:
## Date range
/// marimo-embed
```python
@app.cell
def __():
date_range = mo.ui.date_range(label="Start Date")
return
@app.cell
def __():
mo.hstack([date_range, mo.md(f"Has value: {date_range.value}")])
return
```
///
::: marimo.ui.date_range
:members:
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/dataframe.md
```md
# Dataframe
The dataframe UI element outputs a visual editor to apply "transforms" to a dataframe, such as filtering rows, applying group-bys and aggregations, and more. The transformed dataframe is shown below the transform editor. The UI output also includes the generated Python used to generate the resulting dataframe, which you can copy paste into a cell. You can programmatically access the resulting dataframe by accessing the element's `.value` attribute.
!!! note "Pandas or Polars Required"
In order to use the dataframe UI element, you must have the `pandas` or `polars` package installed.
You can install it with `pip install pandas` or `pip install polars`.
Supported transforms are:
- Filter Rows
- Rename Column
- Column Conversion
- Sort Column
- Group By
- Aggregate
/// marimo-embed
size: large
app_width: full
```python
@app.cell
def __():
import pandas as pd
import pyodide
csv = pyodide.http.open_url("https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv")
df = pd.read_csv(csv)
mo.ui.dataframe(df)
return
```
///
::: marimo.ui.dataframe
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/dictionary.md
```md
# Dictionary
/// marimo-embed
```python
@app.cell
def __():
first_name = mo.ui.text(placeholder="First name")
last_name = mo.ui.text(placeholder="Last name")
email = mo.ui.text(placeholder="Email", kind="email")
dictionary = mo.ui.dictionary(
{
"First name": first_name,
"Last name": last_name,
"Email": email,
}
)
return
@app.cell
def __():
mo.hstack(
[dictionary, dictionary.value],
justify="space-between"
)
return
```
///
::: marimo.ui.dictionary
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/form.md
```md
# Form
/// marimo-embed
size: medium
```python
@app.cell
def __():
form = mo.ui.text_area(placeholder="...").form()
return
@app.cell
def __():
mo.vstack([form, mo.md(f"Has value: {form.value}")])
return
```
///
::: marimo.ui.form
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/index.md
```md
# Inputs
marimo comes packaged with interactive UI elements that you can use to build
powerful notebooks and apps. These elements are available in `marimo.ui`.
| Element | Description |
|---------|-------------|
| [`marimo.ui.array`][marimo.ui.array] | Create array inputs |
| [`marimo.ui.batch`][marimo.ui.batch] | Batch operations |
| [`marimo.ui.button`][marimo.ui.button] | Create buttons |
| [`marimo.ui.chat`][marimo.ui.chat] | Create chat interfaces |
| [`marimo.ui.checkbox`][marimo.ui.checkbox] | Create checkboxes |
| [`marimo.ui.code_editor`][marimo.ui.code_editor] | Create code editors |
| [`marimo.ui.dataframe`][marimo.ui.dataframe] | Interactive dataframes |
| [`marimo.ui.data_explorer`][marimo.ui.data_explorer] | Explore data |
| [`marimo.ui.date`][marimo.ui.date] | Date picker |
| [`marimo.ui.datetime`][marimo.ui.datetime] | Date and time picker |
| [`marimo.ui.date_range`][marimo.ui.date_range] | Date range picker |
| [`marimo.ui.dictionary`][marimo.ui.dictionary] | Dictionary inputs |
| [`marimo.ui.dropdown`][marimo.ui.dropdown] | Create dropdowns |
| [`marimo.ui.file`][marimo.ui.file] | File uploads |
| [`marimo.ui.file_browser`][marimo.ui.file_browser] | Browse files |
| [`marimo.ui.form`][marimo.ui.form] | Create forms |
| [`marimo.ui.microphone`][marimo.ui.microphone] | Record audio |
| [`marimo.ui.multiselect`][marimo.ui.multiselect] | Multiple selection |
| [`marimo.ui.number`][marimo.ui.number] | Number inputs |
| [`marimo.ui.radio`][marimo.ui.radio] | Radio buttons |
| [`marimo.ui.range_slider`][marimo.ui.range_slider] | Range sliders |
| [`marimo.ui.refresh`][marimo.ui.refresh] | Refresh buttons |
| [`marimo.ui.run_button`][marimo.ui.run_button] | Run buttons |
| [`marimo.ui.slider`][marimo.ui.slider] | Create sliders |
| [`marimo.ui.switch`][marimo.ui.switch] | Toggle switches |
| [`marimo.ui.tabs`][marimo.ui.tabs] | Tabbed interfaces |
| [`marimo.ui.table`][marimo.ui.table] | Interactive tables |
| [`marimo.ui.text`][marimo.ui.text] | Text inputs |
| [`marimo.ui.text_area`][marimo.ui.text_area] | Multiline text inputs |
To use a UI element, assign it to a global variable and output it in a cell.
When you interact with the frontend element, the Python object's `value`
attribute is automatically updated, and all cells referencing that object
automatically run with the element's latest value.
## Integrations
| Integration | Description |
|-------------|-------------|
| [`marimo.ui.altair_chart`][marimo.ui.altair_chart] | Interactive Altair charts |
| [`marimo.ui.plotly`][marimo.ui.plotly] | Interactive Plotly charts |
| [`marimo.mpl.interactive`][marimo.mpl.interactive] | Interactive Matplotlib plots |
| [`marimo.ui.anywidget`][marimo.ui.anywidget] | Custom widgets |
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/dropdown.md
```md
# Dropdown
/// marimo-embed
```python
@app.cell
def __():
dropdown = mo.ui.dropdown(options=["Apples", "Oranges", "Pears"], label="choose fruit")
dropdown_dict = mo.ui.dropdown(options={"Apples":1, "Oranges":2, "Pears":3},
value="Apples", # initial value
label="choose fruit with dict options")
return
@app.cell
def __():
mo.vstack([mo.hstack([dropdown, mo.md(f"Has value: {dropdown.value}")]),
mo.hstack([dropdown_dict, mo.md(f"Has value: {dropdown_dict.value} and selected_key {dropdown_dict.selected_key}")]),
])
return
```
///
::: marimo.ui.dropdown
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/file.md
```md
# File
/// marimo-embed
```python
@app.cell
def __():
mo.vstack([mo.ui.file(kind="button"), mo.ui.file(kind="area")])
return
```
///
::: marimo.ui.file
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/file_browser.md
```md
# File Browser
/// marimo-embed
```python
@app.cell
def __():
mo.vstack([mo.ui.file_browser()])
return
```
///
::: marimo.ui.file_browser
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/nav_menu.md
```md
# Navigation Menu
/// marimo-embed
size: large
```python
@app.cell
def __():
nav_menu = mo.nav_menu({
"/overview": "Overview",
"Sales": {
"/sales": {
"label": "Sales",
"description": "View sales and revenue",
},
"/sales/invoices": {
"label": "Invoices",
"description": "View invoices and payments",
},
"/sales/customers": {
"label": "Customers",
"description": "View customers and subscriptions",
},
},
"Products": {
"/products": {
"label": "Products",
"description": "View and manage products",
},
"/products/inventory": {
"label": "Inventory",
"description": "View inventory and stock levels",
},
"/products/categories": {
"label": "Categories",
"description": "View categories and products",
},
},
})
nav_menu
return
```
///
::: marimo.nav_menu
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/multiselect.md
```md
# Multiselect
/// marimo-embed
```python
@app.cell
def __():
options = ["Apples", "Oranges", "Pears"]
multiselect = mo.ui.multiselect(options=options)
return
@app.cell
def __():
mo.hstack([multiselect, mo.md(f"Has value: {multiselect.value}")])
return
```
///
::: marimo.ui.multiselect
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/microphone.md
```md
# Microphone
/// marimo-embed
```python
@app.cell
def __():
microphone = mo.ui.microphone(label="Drop a beat!")
return
@app.cell
def __():
mo.hstack([microphone, mo.audio(microphone.value)])
return
```
///
::: marimo.ui.microphone
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/number.md
```md
# Number
/// marimo-embed
```python
@app.cell
def __():
number = mo.ui.number(start=1, stop=20, label="Number")
return
@app.cell
def __():
mo.hstack([number, mo.md(f"Has value: {number.value}")])
return
```
///
::: marimo.ui.number
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/range_slider.md
```md
# Range Slider
/// marimo-embed
```python
@app.cell
def __():
range_slider = mo.ui.range_slider(start=1, stop=10, step=2, value=[2, 6], full_width=True)
return
@app.cell
def __():
mo.hstack([range_slider, mo.md(f"Has value: {range_slider.value}")])
return
```
///
::: marimo.ui.range_slider
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/run_button.md
```md
# Run Button
/// marimo-embed-file
filepath: examples/ui/run_button.py
///
::: marimo.ui.run_button
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/radio.md
```md
# Radio
/// marimo-embed
```python
@app.cell
def __():
options = ["Apples", "Oranges", "Pears"]
radio = mo.ui.radio(options=options)
return
@app.cell
def __():
mo.hstack([radio, mo.md(f"Has value: {radio.value}")])
return
```
///
::: marimo.ui.radio
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/refresh.md
```md
# Refresh
/// marimo-embed
```python
@app.cell
def __():
refresh = mo.ui.refresh(
label="Refresh",
options=["1s", "5s", "10s", "30s"]
)
return
@app.cell
def __():
mo.hstack([refresh, refresh.value])
return
```
///
::: marimo.ui.refresh
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/switch.md
```md
# Switch
/// marimo-embed
```python
@app.cell
def __():
switch = mo.ui.switch(label="do not disturb")
return
@app.cell
def __():
mo.hstack([switch, mo.md(f"Has value: {switch.value}")])
return
```
///
::: marimo.ui.switch
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/slider.md
```md
# Slider
/// marimo-embed
```python
@app.cell
def __():
slider = mo.ui.slider(start=1, stop=20, label="Slider", value=3)
return
@app.cell
def __():
mo.hstack([slider, mo.md(f"Has value: {slider.value}")])
return
@app.cell
def __():
# You can also use steps to create a slider on a custom range
log_slider = mo.ui.slider(steps=np.logspace(-2, 2, 101), label="Logarithmic Slider", value=1)
return
@app.cell
def __():
mo.hstack([log_slider, mo.md(f"Has value: {log_slider.value}")])
return
@app.cell
def __():
import numpy as np
return
```
///
::: marimo.ui.slider
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/table.md
```md
# Table
/// marimo-embed
size: large
```python
@app.cell
def __():
table = mo.ui.table(data=office_characters, pagination=True)
return
@app.cell
def __():
mo.vstack([table, table.value])
return
@app.cell
def __():
office_characters = [
{"first_name": "Michael", "last_name": "Scott"},
{"first_name": "Jim", "last_name": "Halpert"},
{"first_name": "Pam", "last_name": "Beesly"},
{"first_name": "Dwight", "last_name": "Schrute"},
{"first_name": "Angela", "last_name": "Martin"},
{"first_name": "Kevin", "last_name": "Malone"},
{"first_name": "Oscar", "last_name": "Martinez"},
{"first_name": "Stanley", "last_name": "Hudson"},
{"first_name": "Phyllis", "last_name": "Vance"},
{"first_name": "Meredith", "last_name": "Palmer"},
{"first_name": "Creed", "last_name": "Bratton"},
{"first_name": "Ryan", "last_name": "Howard"},
{"first_name": "Kelly", "last_name": "Kapoor"},
{"first_name": "Toby", "last_name": "Flenderson"},
{"first_name": "Darryl", "last_name": "Philbin"},
{"first_name": "Erin", "last_name": "Hannon"},
{"first_name": "Andy", "last_name": "Bernard"},
{"first_name": "Jan", "last_name": "Levinson"},
{"first_name": "David", "last_name": "Wallace"},
{"first_name": "Holly", "last_name": "Flax"},
]
return
```
///
::: marimo.ui.table
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/tabs.md
```md
# Tabs
/// marimo-embed
size: large
```python
@app.cell
def __():
import matplotlib.pyplot as plt
import numpy as np
# Generate some random data
categories = ["A", "B", "C", "D", "E"]
values = np.random.rand(5)
bar = plt.bar(categories, values)
plt.title("Random Bar Chart")
plt.xlabel("Categories")
plt.ylabel("Values")
def simple_echo_model(messages, config):
return f"You said: {messages[-1].content}"
chat = mo.ui.chat(
simple_echo_model,
prompts=["Hello", "How are you?"],
show_configuration_controls=True
)
None
return
@app.cell
def __():
mo.ui.tabs(
{
"📈 Sales": bar,
"📊 Chatbot": chat,
"💻 Settings": mo.ui.text(placeholder="Key"),
}
)
return
```
///
::: marimo.ui.tabs
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/accordion.md
```md
# Accordion
/// marimo-embed
size: medium
```python
@app.cell
def __():
mo.accordion(
{
"Door 1": mo.md("Nothing!"),
"Door 2": mo.md("Nothing!"),
"Door 3": mo.md(
"![goat](https://images.unsplash.com/photo-1524024973431-2ad916746881)"
),
}
)
return
```
///
::: marimo.accordion
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/text_area.md
```md
# Text Area
/// marimo-embed-file
filepath: examples/ui/text_area.py
///
::: marimo.ui.text_area
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/carousel.md
```md
# Carousel
/// marimo-embed
size: large
```python
@app.cell
def __():
mo.carousel([
mo.md("# Introduction"),
"By the marimo team",
mo.md("## What is marimo?"),
mo.md("![marimo moss ball](https://marimo.io/logo.png)"),
mo.md("## Questions?"),
])
return
```
///
::: marimo.carousel
```
File: /Users/morganmcguire/ML/marimo/docs/api/inputs/text.md
```md
# Text
/// marimo-embed
```python
@app.cell
def __():
text = mo.ui.text(placeholder="Search...", label="Filter")
return
@app.cell
def __():
mo.hstack([text, mo.md(f"Has value: {text.value}")])
return
```
///
::: marimo.ui.text
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/callout.md
```md
# Callout
/// marimo-embed
```python
@app.cell
def __():
callout_kind = mo.ui.dropdown(
label="Color",
options=["info", "neutral", "danger", "warn", "success"],
value="neutral",
)
return
@app.cell
def __():
callout = mo.callout("This is a callout", kind=callout_kind.value)
return
@app.cell
def __():
mo.vstack([callout_kind, callout], align="stretch", gap=0)
return
```
///
::: marimo.callout
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/index.md
```md
# Layouts
marimo has higher-order layout functions that you can use to arrange outputs
in rows, columns, tables, tabs, and more.
## Stateless
Unlike elements in `marimo.ui`, these don't have any values associated with
them but just render their children in a certain way.
| Function | Description |
|----------|-------------|
| [`marimo.accordion`][marimo.accordion] | Create collapsible sections |
| [`marimo.carousel`][marimo.carousel] | Create a slideshow |
| [`marimo.callout`][marimo.callout] | Create highlighted sections |
| [`marimo.center`][marimo.center] | Center content |
| [`marimo.hstack`][marimo.hstack] | Stack elements horizontally |
| [`marimo.lazy`][marimo.lazy] | Lazy load content |
| [`marimo.left`][marimo.left] | Left-align content |
| [`marimo.nav_menu`][marimo.nav_menu] | Create navigation menus |
| [`marimo.plain`][marimo.plain] | Display content without styling |
| [`marimo.right`][marimo.right] | Right-align content |
| [`marimo.routes`][marimo.routes] | Create page routing |
| [`marimo.sidebar`][marimo.sidebar] | Create sidebars |
| [`marimo.tree`][marimo.tree] | Create tree structures |
| [`marimo.vstack`][marimo.vstack] | Stack elements vertically |
## Stateful
Some elements in `marimo.ui` are also helpful for layout. These elements
do have values associated with them: for example, `tabs` tracks the
selected tab name, and `table` tracks the selected rows.
| Function | Description |
|----------|-------------|
| [`marimo.ui.tabs`][marimo.ui.tabs] | Create tabbed interfaces |
| [`marimo.ui.table`][marimo.ui.table] | Create interactive tables |
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/lazy.md
```md
# Lazy
/// marimo-embed
```python
@app.cell
def __():
mo.accordion({
"Open me": mo.lazy(expensive_number, show_loading_indicator=True)
})
return
@app.cell
def __():
import time
import random
def expensive_number():
time.sleep(1)
num = random.randint(0, 100)
return num
return
```
///
::: marimo.lazy
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/tree.md
```md
# Tree
/// marimo-embed
```python
@app.cell
def __():
mo.tree(
["entry", "another entry", {"key": [0, mo.ui.slider(1, 10, value=5), 2]}],
label="A tree of elements.",
)
return
```
///
::: marimo.tree
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/stacks.md
```md
# Stacks
/// marimo-embed
size: large
```python
@app.cell
def __():
def create_box(num=1):
box_size = 30 + num * 10
return mo.Html(
f"<div style='min-width: {box_size}px; min-height: {box_size}px; background-color: orange; text-align: center; line-height: {box_size}px'>{str(num)}</div>"
)
boxes = [create_box(i) for i in range(1, 5)]
return
@app.cell
def __():
justify = mo.ui.dropdown(
["start", "center", "end", "space-between", "space-around"],
value="space-between",
label="justify",
)
align = mo.ui.dropdown(
["start", "center", "end", "stretch"], value="center", label="align"
)
gap = mo.ui.number(start=0, step=0.25, stop=2, value=0.25, label="gap")
wrap = mo.ui.checkbox(label="wrap")
return
@app.cell
def __():
horizontal = mo.hstack(
boxes,
align=align.value,
justify=justify.value,
gap=gap.value,
wrap=wrap.value,
)
vertical = mo.vstack(
boxes,
align=align.value,
gap=gap.value,
)
mo.vstack(
[
mo.hstack([justify, align, gap], justify="center"),
horizontal,
mo.md("-----------------------------"),
vertical,
],
align="stretch",
gap=1,
)
return
```
///
::: marimo.hstack
::: marimo.vstack
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/plain.md
```md
# Plain
::: marimo.plain
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/justify.md
```md
# Justify
::: marimo.center
::: marimo.left
::: marimo.right
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/routes.md
```md
# Routes
::: marimo.routes
python
import marimo
app = marimo.App()
@app.cell
def __():
import marimo as mo
return
@app.cell
def __():
mo.sidebar(
[
mo.md("# marimo"),
mo.nav_menu(
{
"#/": f"{mo.icon('lucide:home')} Home",
"#/about": f"{mo.icon('lucide:user')} About",
"#/contact": f"{mo.icon('lucide:phone')} Contact",
"Links": {
"https://twitter.com/marimo_io": "Twitter",
"https://github.com/marimo-team/marimo": "GitHub",
},
},
orientation="vertical",
),
]
)
return
@app.cell
def __():
mo.routes({
"#/": mo.md("# Home"),
"#/about": mo.md("# About"),
"#/contact": mo.md("# Contact"),
mo.routes.CATCH_ALL: mo.md("# Home"),
})
return
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/audio.md
```md
# Audio
/// marimo-embed
```python
@app.cell
def __():
_src = "https://upload.wikimedia.org/wikipedia/commons/8/8c/Ivan_Ili%C4%87-Chopin_-_Prelude_no._1_in_C_major.ogg"
mo.audio(_src)
return
```
///
::: marimo.audio
```
File: /Users/morganmcguire/ML/marimo/docs/api/layouts/sidebar.md
```md
# Sidebar
/// marimo-embed
size: medium
```python
@app.cell
def __():
mo.sidebar(
[
mo.md("# marimo"),
mo.nav_menu(
{
"#/home": f"{mo.icon('lucide:home')} Home",
"#/about": f"{mo.icon('lucide:user')} About",
"#/contact": f"{mo.icon('lucide:phone')} Contact",
"Links": {
"https://twitter.com/marimo_io": "Twitter",
"https://github.com/marimo-team/marimo": "GitHub",
},
},
orientation="vertical",
),
]
)
return
```
///
::: marimo.sidebar
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/download.md
```md
# Download Media
/// marimo-embed-file
filepath: examples/ui/download.py
///
::: marimo.download
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/image.md
```md
# Image
/// marimo-embed
```python
@app.cell
def __():
_src = (
"https://images.pexels.com/photos/86596/owl-bird-eyes-eagle-owl-86596.jpeg"
)
mo.image(src=_src, width="180px", height="180px", rounded=True)
return
```
///
::: marimo.image
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/video.md
```md
# Video
/// marimo-embed
size: medium
```python
@app.cell
def __():
mo.video(
src="https://v3.cdnpk.net/videvo_files/video/free/2013-08/large_watermarked/hd0992_preview.mp4",
controls=False,
)
return
```
///
::: marimo.video
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/index.md
```md
# Media
Use these functions to embed media in your outputs.
| Function | Description |
|----------|-------------|
| [`marimo.image`](image.md) | Display images |
| [`marimo.audio`](audio.md) | Play audio files |
| [`marimo.video`](video.md) | Play videos |
| [`marimo.pdf`](pdf.md) | Display PDFs |
| [`marimo.download`](download.md) | Create download links |
| [`marimo.plain_text`](plain_text.md) | Display plain text |
Most of these methods accept URLs (including data URLs), paths to local
files, or file-like objects.
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/pdf.md
```md
# PDF
::: marimo.pdf
```
File: /Users/morganmcguire/ML/marimo/docs/api/media/plain_text.md
```md
# Plain text
::: marimo.plain_text
```
File: /Users/morganmcguire/ML/marimo/docs/api/cell.md
```md
# Cell
::: marimo.Cell
```
File: /Users/morganmcguire/ML/marimo/docs/api/app.md
```md
# App
::: marimo.App
options:
members:
- embed
## AppMeta
::: marimo.app_meta
```
File: /Users/morganmcguire/ML/marimo/docs/api/cli_args.md
```md
# Command Line Arguments
Use `mo.cli_args` to access command-line arguments passed to the notebook. This
allows you to pass arguments to the notebook that are not controllable by the
user. The arguments will be parsed from the command line when running the
notebook as an application with `marimo run` or `marimo edit`; they will also
be parsed from the command line when running as a script.
Some examples passing command-line arguments to the notebook when running
as a script:
```bash
python notebook.py -- --arg1 value1 --arg2 value2
# mo.cli_args() == {'arg1': 'value1', 'arg2': 'value2'}
python notebook.py -- --arg1=10 --arg2=true --arg3
# mo.cli_args() == {'arg1': 10, 'arg2': True, 'arg3': ''}
python notebook.py -- --arg1 10.5 --arg2 hello --arg2 world
# mo.cli_args() == {'arg1': 10.5, 'arg2': ['hello', 'world']}
```
In each example, `python` can be replaced as `marimo run` (for running as
an app) or `marimo edit` (for running as a notebook).
::: marimo.cli_args
!!! note "Query Parameters"
You can also access query parameters passed to the notebook using
`mo.query_params`. This allows you to pass arguments to the notebook that can be controlled by the user.
```
File: /Users/morganmcguire/ML/marimo/docs/api/html.md
```md
# HTML
All marimo elements extend the HTML element class.
::: marimo.as_html
::: marimo.Html
::: marimo.iframe
```
File: /Users/morganmcguire/ML/marimo/docs/api/caching.md
```md
# Caching
marimo comes with utilities to cache intermediate computations. These utilities
come in two types: caching the return values of expensive functions in memory,
and caching the values of variables to disk.
## Caching expensive functions
Use [`mo.cache`][marimo.cache] to cache the return values of functions in
memory, based on the function arguments, closed-over values, and the notebook
code defining the function.
The resulting cache is similar to `functools.cache`, but with the benefit that
[`mo.cache`][marimo.cache] won't return stale values (because it keys on
closed-over values) and isn't invalidated when the cell defining the decorated
function is simply re-run (because it keys on notebook code). This means that
like marimo notebooks, [`mo.cache`][marimo.cache] has no hidden state
associated with the cached function, which makes you more productive while developing iteratively.
For a cache with bounded size, use [`mo.lru_cache`][marimo.lru_cache].
::: marimo.cache
::: marimo.lru_cache
## Caching variables to disk
Use [`mo.persistent_cache`][marimo.persistent_cache] to cache variables computed in an expensive block of
code to disk. The next time this block of code is run, if marimo detects a
cache hit, the code will be skipped and your variables will be loaded into
memory, letting you pick up where you left off.
!!! tip "Cache location"
By default, caches are stored in `__marimo__/cache/`, in the directory of the
current notebook. For projects versioned with `git`, consider adding
`**/__marimo__/cache/` to your `.gitignore`.
::: marimo.persistent_cache
```
File: /Users/morganmcguire/ML/marimo/docs/api/diagrams.md
```md
# Diagrams
/// marimo-embed
size: medium
```python
@app.cell
def __():
mo.mermaid("graph TD\n A[Christmas] -->|Get money| B(Go shopping)\n B --> C{Let me think}\n C -->|One| D[Laptop]\n C -->|Two| E[iPhone]\n C -->|Three| F[Car]")
return
```
///
## Mermaid diagrams
::: marimo.mermaid
```
File: /Users/morganmcguire/ML/marimo/docs/api/index.md
```md
# API Reference
Use the marimo library in marimo notebooks (`import marimo as mo`) to
- connect interactive inputs like sliders, dropdowns, and tables to Python,
- express yourself with dynamically created markdown,
- layout information with tabs or grids,
- output media like images and audio,
- and more!
| | |
| :------------------- | :-------------------------------------------------------- |
| [markdown](markdown.md) | Write markdown with `mo.md` |
| [inputs](inputs/index.md) | Connect sliders, dropdowns, tables, and more to Python |
| [layouts](layouts/index.md) | Customize outputs with accordions, tabs, stacks, and more |
| [plotting](plotting.md) | Output interactive plots |
| [media](media/index.md) | Output media like images, audio, PDFs, and plain text |
| [diagrams](diagrams.md) | Flow charts, graphs, statistic cards, and more |
| [status](status.md) | Display progress indicators |
| [outputs](outputs.md) | Modify cell outputs, redirect console output |
| [control_flow](control_flow.md) | Control how cells execute |
| [html](html.md) | Manipulate HTML objects |
| [query_params](query_params.md) | Access and set query parameters with `mo.query_params` |
| [cli_args](cli_args.md) | Access command-line arguments with `mo.cli_args` |
| [caching](caching.md) | Cache expensive computations in memory or on disk |
| [state](state.md) | Synchronize multiple UI elements with `mo.state` |
| [app](app.md) | Embed notebooks in other notebooks |
| [cell](cell.md) | Run cells defined in another notebook |
| [miscellaneous](miscellaneous.md) | Miscellaneous utilities |
```
File: /Users/morganmcguire/ML/marimo/docs/api/control_flow.md
```md
# Control flow
Use `mo.stop` to halt execution of a cell, and optionally output an object.
This function is useful for validating user input.
::: marimo.stop
Use [`mo.ui.refresh`][marimo.ui.refresh] to trigger other cells to run periodically, on a configurable
interval (or on click).
```
File: /Users/morganmcguire/ML/marimo/docs/api/markdown.md
```md
# Markdown
Write markdown with `mo.md`; make your markdown **interactive**, **dynamic**,
and **visually rich** by interpolating arbitrary Python values and marimo
elements.
::: marimo.md
## Icons
We support rendering icons from [Iconify](https://icon-sets.iconify.design/).
When is inside markdown, you can render an icon with the syntax `::iconset:icon-name::` for example `::lucide:rocket::` or `::mdi:home::`. This is useful for quickly adding an icon, however, it does not support advanced configuration such as size, color, and rotation.
For other advanced features, use `mo.icon()` such as `mo.icon("lucide:rocket", size=20)` or `mo.icon("mdi:home", color="blue")`.
::: marimo.icon
## Tooltips
You can render a tooltip by adding the `data-tooltip` attribute to an element.
```python
mo.md(
'''
<div data-tooltip="This is a tooltip">Hover over me</div>
'''
)
mo.ui.button(
label='<div data-tooltip="This is a tooltip">Hover over me</div>'
)
```
## Rendering images
You can render images from a local `public/` folder:
```python
mo.md(
'''
<img src="public/image.png" width="100" />
'''
)
```
See [Static files](../guides/outputs.md#static-files) for information about serving images and other static assets.
```
File: /Users/morganmcguire/ML/marimo/docs/api/miscellaneous.md
```md
# Miscellaneous
::: marimo.running_in_notebook
::: marimo.defs
::: marimo.refs
::: marimo.notebook_dir
::: marimo.notebook_location
::: marimo.Thread
```
File: /Users/morganmcguire/ML/marimo/docs/api/outputs.md
```md
# Outputs
## Cell outputs
!!! note "Cell outputs"
Every cell in a marimo notebook can have a visual **output**. When editing,
outputs are displayed above cells. When running a notebook as an app,
its UI is an arrangement of outputs.
A cell's output is by default its last expression. You can also create outputs
programmatically, using `mo.output.replace()` and `mo.output.append()`.
::: marimo.output.replace
::: marimo.output.append
::: marimo.output.clear
::: marimo.output.replace_at_index
!!! warning "Last expression replaces existing output"
Ending a cell with a non-`None` expression is the same as calling
`mo.output.replace()` on it: the last expression replaces any output you may have
already written. Wrap the last expression in `mo.output.append` if you want
to add to an existing output instead of replacing it.
### Display cell code in marimo's app views
Use `mo.show_code()` to display the cell's code in the output area, which
will then be visible in all app views.
::: marimo.show_code
## Console outputs
/// admonition | Console outputs
type: note
Text written to `stdout`/`stderr`, including print statements
and logs, shows up in a console output area below a cell.
By default, these console outputs don't appear when running a marimo notebook
as an app. If you do want them to appear in apps, marimo provides utility
functions for capturing console outputs and redirecting them to cell outputs.
///
::: marimo.redirect_stdout
::: marimo.redirect_stderr
::: marimo.capture_stdout
::: marimo.capture_stderr
```
File: /Users/morganmcguire/ML/marimo/docs/api/state.md
```md
# State
!!! warning "Stop! Read the interactivity guide first!"
**Read the guide on [creating interactive
elements](../guides/interactivity.md)** before reading this one!
!!! warning "Advanced topic!"
This guide covers reactive state (`mo.state`), an advanced topic.
**You likely don't need `mo.state`**. UI elements already have built-in
state, their associated value, which you can access with their `value` attribute.
For example, `mo.ui.slider()` has a value that is its current position on an
interval, while `mo.ui.button()` has a value that can be configured to
count the number of times it has been clicked, or to toggle between `True` and
`False`. Additionally, interacting with UI elements bound to global variables
[automatically executes cells](../guides/interactivity.md) that reference those
variables, letting you react to changes by just reading their
`value` attributes. **This functional paradigm is the preferred way of
reacting to UI interactions in marimo.** **Chances are, the reactive
execution built into UI elements will suffice.** (For example, [you don't need
reactive state to handle a button click](../recipes.md#working-with-buttons).)
That said, here are some signs you might need `mo.state`:
- you need to maintain historical state related to a UI element that can't
be computed from its built-in `value` (_e.g._, all values the user has
ever input into a form)
- you need to synchronize two different UI elements (_e.g._, so that
interacting with either one controls the other)
- you need to introduce cycles across cells
**In over 99% of cases, you don't need and shouldn't use `mo.state`.** This
feature can introduce hard-to-find bugs.
::: marimo.state
```
File: /Users/morganmcguire/ML/marimo/docs/api/plotting.md
```md
# Plotting
marimo supports most major plotting libraries, including Matplotlib, Seaborn,
Plotly, and Altair. Just import your plotting library of choice and use it
as you normally would.
For more information about plotting, see the [plotting guide](../guides/working_with_data/plotting.md).
## Reactive charts with Altair
/// marimo-embed
size: large
```python
@app.cell
async def __():
import pandas as pd
import pyodide
import micropip
import json
await micropip.install('altair')
import altair as alt
return
@app.cell
def __():
cars = pd.DataFrame(json.loads(
pyodide.http.open_url('https://vega.github.io/vega-datasets/data/cars.json').read()
))
chart = mo.ui.altair_chart(alt.Chart(cars).mark_point().encode(
x='Horsepower',
y='Miles_per_Gallon',
color='Origin'
))
return
@app.cell
def __():
mo.vstack([chart, mo.ui.table(chart.value)])
return
```
///
### Disabling automatic selection
marimo automatically adds a default selection based on the mark type, however, you may want to customize the selection behavior of your Altair chart. You can do this by setting `chart_selection` and `legend_selection` to `False`, and using `.add_params` directly on your Altair chart.
```python
# Create an interval selection
brush = alt.selection_interval(encodings=["x"])
_chart = (
alt.Chart(traces, height=150)
.mark_line()
.encode(x="index:Q", y="value:Q", color="traces:N")
.add_params(brush) # add the selection to the chart
)
chart = mo.ui.altair_chart(
_chart,
# disable automatic selection
chart_selection=False,
legend_selection=False
)
chart # You can now access chart.value to get the selected data
```
::: marimo.ui.altair_chart
### Performance and Data Transformers
Altair has a concept of [data](https://altair-viz.github.io/user_guide/data_transformers.html) transformers, which can be used to improve performance.
Such examples are:
- pandas Dataframe has to be sanitized and serialized to JSON.
- The rows of a Dataframe might need to be sampled or limited to a maximum number.
- The Dataframe might be written to a `.csv` or `.json` file for performance reasons.
By default, Altair uses the `default` data transformer, which is the slowest in marimo. It is limited to 5000 rows (although we increase this to `20_000` rows as marimo can handle this). This includes the data inside the HTML that is being sent over the network, which can also be limited by marimo's maximum message size.
It is recommended to use the `marimo_csv` data transformer, which is the most performant and can handle the largest datasets: it converts the data to a CSV file which is smaller and can be sent over the network. This can handle up to +400,000 rows with no issues.
When using `mo.ui.altair_chart`, we automatically set the data transformer to `marimo_csv` for you. If you are using Altair directly, you can set the data transformer using the following code:
```python
import altair as alt
alt.data_transformers.enable('marimo_csv')
```
## Reactive plots with Plotly
!!! warning "mo.ui.plotly only supports scatter plots, treemaps charts, and sunbursts charts."
marimo can render any Plotly plot, but [`mo.ui.plotly`][marimo.ui.plotly] only
supports reactive selections for scatter plots, treemaps charts, and sunbursts charts. If you require other kinds of
selection, consider using [`mo.ui.altair_chart`][marimo.ui.altair_chart].
::: marimo.ui.plotly
## Interactive matplotlib
::: marimo.mpl.interactive
options:
show_root_heading: true
show_source: true
## Leafmap support
marimo supports rendering [Leafmap](https://leafmap.org/) maps using the `folium` and `plotly` backends.
## Other plotting libraries
You can use all the popular plotting libraries with marimo. Such as:
- [Matplotlib](https://matplotlib.org/)
- [Plotly](https://plotly.com/)
- [Seaborn](https://seaborn.pydata.org/)
- [Bokeh](https://bokeh.org/)
- [Altair](https://altair-viz.github.io/)
- [HoloViews](http://holoviews.org/)
- [hvPlot](https://hvplot.holoviz.org/)
- [Leafmap](https://leafmap.org/)
- [Pygwalker](https://kanaries.net/pygwalker)
```
File: /Users/morganmcguire/ML/marimo/docs/apps/embedding_numbers.py
```py
import marimo
__generated_with = "0.7.19"
app = marimo.App()
@app.cell
def __(mo):
mo.md(
"""
# Embedding MNIST
This app shows how to use the function `pymde.preserve_neighbors`
to produce embeddings that highlight the local structure of your
data, using MNIST as a case study. In these embeddings similar
digits are near each other, and dissimilar digits are not near each other.
## Data
The data we'll embed are 70,000 28x28 grayscale images of handwritten
digits:
"""
)
return
@app.cell
def __(button, show_random_images):
button
show_random_images(5)
return
@app.cell
def __(mo):
button = mo.ui.button(label="Click this button")
mo.md(f"{button} _to view another random sample of images._").center()
return button,
@app.cell
def __(mo):
params = (
mo.md(
"""
### Try these controls <span style="font-size: 28px">🎮</span>
Here are some parameters you can play around with to control the
embedding.
- embedding dimension (2 or 3): {embedding_dimension}
- constraint: {constraint_type}
"""
)
.batch(
embedding_dimension=mo.ui.slider(2, 3, value=2),
constraint_type=mo.ui.dropdown(
("Centered", "Standardized"), value="Centered"
),
)
.form()
)
params
return params,
@app.cell
def __(params):
if params.value is not None:
embedding_dimension, constraint_type = (
params.value["embedding_dimension"],
params.value["constraint_type"],
)
else:
embedding_dimension, constraint_type = None, None
return constraint_type, embedding_dimension
@app.cell
def __(constraint_type, pymde):
if constraint_type is not None:
_constraints = {
"Centered": pymde.Centered(),
"Standardized": pymde.Standardized(),
}
constraint = _constraints[constraint_type]
return constraint,
@app.cell
def __(
compute_embedding,
constraint,
embedding_dimension,
mnist,
plt,
pymde,
):
def show_embedding():
_, embedding = compute_embedding(embedding_dimension, constraint)
pymde.plot(embedding, color_by=mnist.attributes["digits"])
plt.tight_layout()
return plt.gca()
show_embedding() if embedding_dimension is not None else None
return show_embedding,
@app.cell
def __(mnist, pymde, torch):
embedding_cache = {}
def compute_embedding(embedding_dim, constraint):
key = (embedding_dim, constraint)
if key in embedding_cache:
return embedding_cache[key]
mde = pymde.preserve_neighbors(
mnist.data,
embedding_dim=embedding_dim,
constraint=constraint,
device="cuda" if torch.cuda.is_available() else "cpu",
verbose=True,
)
X = mde.embed(verbose=True)
value = (mde, X)
embedding_cache[key] = value
return value
return compute_embedding, embedding_cache
@app.cell
def __(pymde):
mnist = pymde.datasets.MNIST()
return mnist,
@app.cell
def __(mnist, plt, torch):
def show_random_images(n_images):
indices = torch.randperm(mnist.data.shape[0])[:n_images]
images = mnist.data[indices].reshape((-1, 28, 28))
fig, axes = plt.subplots(1, n_images)
fig.set_size_inches(6.5, 1.5)
for im, ax in zip(images, axes.flat):
ax.imshow(im, cmap="gray")
ax.set_yticks([])
ax.set_xticks([])
plt.tight_layout()
return fig
return show_random_images,
@app.cell
def __():
import matplotlib.pyplot as plt
import pymde
import torch
import marimo as mo
return mo, plt, pymde, torch
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/api/status.md
```md
# Status
Use progress bars or spinners to visualize loading status in your notebooks and
apps. Useful when iterating over collections or loading data from files,
databases, or APIs.
## Progress bar
You can display a progress bar while iterating over a collection, similar
to `tqdm`.
/// marimo-embed
size: medium
```python
@app.cell
def __():
rerun = mo.ui.button(label="Rerun")
rerun
return
@app.cell
async def __():
import asyncio
rerun
for _ in mo.status.progress_bar(
range(10),
title="Loading",
subtitle="Please wait",
show_eta=True,
show_rate=True
):
await asyncio.sleep(0.5)
return
```
///
::: marimo.status.progress_bar
## Spinner
/// marimo-embed
size: medium
```python
@app.cell
def __():
rerun = mo.ui.button(label="Rerun")
rerun
return
@app.cell
async def __():
import asyncio
rerun
with mo.status.spinner(title="Loading...") as _spinner:
await asyncio.sleep(1)
_spinner.update("Almost done")
await asyncio.sleep(1)
_spinner.update("Done")
return
```
///
::: marimo.status.spinner
```
File: /Users/morganmcguire/ML/marimo/docs/api/query_params.md
```md
# Query Parameters
Use `mo.query_params` to access query parameters passed to the notebook. You
can also use `mo.query_params` to set query parameters in order to keep track
of state in the URL. This is useful for bookmarking or sharing a particular
state of the notebook while running as an application with `marimo run`.
::: marimo.query_params
!!! note "CLI arguments"
You can also access command-line arguments passed to the notebook using
`mo.cli_args`. This allows you to pass arguments to the notebook that are not controllable by the user.
```
File: /Users/morganmcguire/ML/marimo/docs/apps/motherduck.py
```py
import marimo
__generated_with = "0.9.10"
app = marimo.App(width="full")
@app.cell
def __():
import marimo as mo
import duckdb
return duckdb, mo
@app.cell
def __(duckdb):
duckdb.sql(
"ATTACH 'md:_share/sample_data/23b0d623-1361-421d-ae77-62d701d471e6' AS sample_data"
)
return (sample_data,)
@app.cell
def __(mo):
mo.md(r"""## Reactive SQL""")
return
@app.cell
def __(mo):
last_x_months = mo.ui.slider(24, 30, label="Last x months")
last_x_months
return (last_x_months,)
@app.cell
def __(last_x_months, mo):
recent_hacker_news = mo.sql(
f"""
FROM sample_data.hn.hacker_news
WHERE timestamp >= (CURRENT_DATE - INTERVAL {last_x_months.value} month)
AND type = 'story'
"""
)
return (recent_hacker_news,)
@app.cell
def __(mo, recent_hacker_news):
aggregations = mo.sql(
f"""
SELECT
COUNT(*) AS total_posts, AVG(score) AS avg_score,
MAX(score) AS max_score, MIN(score) AS min_score,
FROM recent_hacker_news WHERE score IS NOT NULL;
"""
)
return (aggregations,)
@app.cell
def __(mo):
mo.md(r"""## Mix and match Python""")
return
@app.cell
def __(mo, sample_data, service_requests):
agency_tickets = mo.sql(
f"""
SELECT
agency_name,
COUNT(*) AS num_requests,
CAST(SUM(CASE WHEN status = 'Closed' THEN 1 ELSE 0 END) AS INT64) AS closed_count,
CAST(SUM(CASE WHEN status = 'Open' THEN 1 ELSE 0 END) AS INT64) AS open_count
FROM sample_data.nyc.service_requests
GROUP BY agency_name ORDER BY closed_count DESC LIMIT 20
"""
)
return (agency_tickets,)
@app.cell
def __(agency_tickets):
import altair as alt
scale = alt.Scale(type="sqrt")
base = (
alt.Chart(agency_tickets)
.encode(y="agency_name", x=alt.X("num_requests", scale=scale))
.properties(width="container")
)
chart_closed = base.mark_bar(color="#FFC080").encode(
x=alt.X("closed_count", scale=scale)
)
chart_open = base.mark_bar(color="#8BC34A").encode(
x=alt.X("open_count", scale=scale)
)
chart_closed + chart_open
return alt, base, chart_closed, chart_open, scale
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/apps/intro.py
```py
import marimo
__generated_with = "0.0.6"
app = marimo.App()
@app.cell
def __(mo):
mo.md("# Welcome to marimo! 🌊🍃")
return
@app.cell
def __(mo):
slider = mo.ui.slider(1, 22)
return slider,
@app.cell
def __(mo, slider):
mo.md(
f"""
marimo is a Python library for creating reactive and interactive
notebooks and apps.
Unlike traditional notebooks, marimo notebooks **run
automatically** when you modify them or
interact with UI elements, like this slider: {slider}.
{"##" + "🍃" * slider.value}
"""
)
return
@app.cell
def __(mo):
mo.accordion(
{
"A notebook or an app?": (
"""
Because marimo notebooks react to changes and UI interactions,
they can also be thought of as apps: click
the app window icon to see an "app view" that
hides code.
Depending on how you use marimo, you can think
of marimo programs as notebooks, apps, or both.
"""
)
}
)
return
@app.cell
def __():
import marimo as mo
return mo,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/apps/readme_ui.py
```py
import marimo
__generated_with = "0.0.13"
app = marimo.App()
@app.cell
def __(mo):
x = mo.ui.slider(1, 9)
x
return x,
@app.cell
def __(math, mo, x):
mo.md(f"$e^{x.value} = {math.exp(x.value):0.3f}$")
return
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
form = mo.ui.slider(1, 9).form()
form
return form,
@app.cell
def __(form, mo):
mo.md(f"The last submitted value is **{form.value}**")
return
@app.cell
def __():
import math
return math,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/apps/output.py
```py
import marimo
__generated_with = "0.0.6"
app = marimo.App()
@app.cell
def __(tabs):
tabs
return
@app.cell
def __(array, ax, dictionary, mo, plots, table, text):
tabs = mo.ui.tabs(
{
"markdown": mo.md(
r"""
Use `mo.md` to write markdown, with support for LaTeX:
\[
e = \sum_{k=0}^{\infty} 1/k!.
\]
"""
+ f"""
You can even interpolate arbitrary Python values and marimo
elements into your markdown. Try typing your name below:
{mo.hstack([
text, mo.md("Hello, " + text.value + "!")
], justify="center")}
"""
).left(),
"lists and dicts": mo.hstack(
[array, dictionary], justify="space-around"
),
"tables": mo.md(
f"""
{table}
Employee of the month: {table.value[0]["first_name"] + "! 🎉" if table.value else ""}
"""
),
"accordion": mo.accordion(
{
"Tip!": f"""
Express yourself with outputs! Put anything in accordions,
like plots:
{mo.as_html(ax)}.
"""
}
),
"rows and columns": plots,
"and more ...": mo.md(
"**Build anything you can imagine**. Check out our tutorials and examples for inspiration."
),
}
)
return tabs,
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
text = mo.ui.text(value="stranger")
return text,
@app.cell
def __(mo):
slider = mo.ui.slider(2, 4, step=1)
return slider,
@app.cell
def __(plt, slider, x):
plt.figure(figsize=(2, 2))
plt.plot(x, x**slider.value)
ax = plt.gca()
ax.set_xticks([])
ax.set_yticks([])
None
return ax,
@app.cell
def __(ax, mo, slider):
array = [slider, mo.md(f"$f(x) = x^{slider.value}$"), ax]
return array,
@app.cell
def __(array, mo):
dictionary = {"md": mo.md("nest lists and dicts!"), "list": array}
return dictionary,
@app.cell
def __(mo):
table = mo.ui.table(
[
{
"first_name": "Michael",
"last_name": "Scott",
"skill": mo.ui.slider(1, 10, value=3),
"favorite place": mo.image(src="https://picsum.photos/100"),
},
{
"first_name": "Jim",
"last_name": "Halpert",
"skill": mo.ui.slider(1, 10, value=7),
"favorite place": mo.image(src="https://picsum.photos/100"),
},
],
selection="single"
)
return table,
@app.cell
def __(mo, np, plt):
def plot(x, y, title):
plt.figure(figsize=(2, 2))
plt.plot(x, y)
plt.title(title)
plt.tight_layout()
return plt.gca()
x = np.linspace(-2, 2, 100)
linear = plot(x, x, "$x$")
quadratic = plot(x, x**2, "$x^2$")
sine = plot(x, np.sin(x), "$\sin(x)$")
cos = plot(x, np.cos(x), "$\cos(x)$")
exp = plot(x, np.exp(x), "$\exp(x)$")
plots = mo.vstack([
mo.hstack([linear, quadratic]),
mo.hstack([sine, cos, exp])
], align="center")
return cos, exp, linear, plot, plots, quadratic, sine, x
@app.cell
def __():
import numpy as np
return np,
@app.cell
def __():
import matplotlib.pyplot as plt
return plt,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/getting_started/index.md
```md
# Getting Started
These tutorials will help you get started with marimo
| Guide | Description |
|-------|-------------|
| [Installation](installation.md) | Installing marimo |
| [Quickstart](quickstart.md) | Create notebooks, run apps, and more from the marimo command-line |
| [Key Concepts](key_concepts.md) | A tour of key features and concepts |
```
File: /Users/morganmcguire/ML/marimo/docs/apps/README.md
```md
# Documentation Apps
This directory contains marimo apps that are used in the documentation.
```
File: /Users/morganmcguire/ML/marimo/docs/apps/sql.py
```py
import marimo
__generated_with = "0.7.1"
app = marimo.App(width="medium")
@app.cell
def __(mo):
cars = mo.sql(
f"""
-- Download a CSV and create an in-memory table
CREATE OR replace TABLE cars as
FROM 'https://datasets.marimo.app/cars.csv';
SELECT Make, Model, Cylinders, Weight, MPG_City from cars;
"""
)
return cars,
@app.cell
def __(cars):
# We can reference the output variable as a dataframe in python
[len(cars), cars["MPG_City"].mean()]
return
@app.cell(hide_code=True)
def __():
import marimo as mo
return mo,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/docs/getting_started/key_concepts.md
```md
# Key concepts
This page covers marimo's key concepts:
* marimo lets you rapidly experiment with data using Python, SQL, and interactive
elements in a reproducible **notebook environment**.
* Unlike Jupyter notebooks, marimo notebooks are reusable software artifacts.
marimo notebooks can be shared as as **interactive web apps** and executed as
**Python scripts**.
## Editing notebooks
marimo notebooks are **reactive**: they automatically react to your code
changes and UI interactions and keep your notebook up-to-date, not unlike a
spreadsheet. This makes your notebooks reproducible, [eliminating hidden
state](../faq.md#faq-problems); it's also what enables marimo notebooks to double as
apps and Python scripts.
!!! important "Working with expensive notebooks"
If you don't want cells to run automatically, the [runtime can be
configured](../guides/configuration/runtime_configuration.md) to be lazy, only
running cells when you ask for them to be run and marking affected cells as
stale. **See our guide on working with [expensive
notebooks](../guides/expensive_notebooks.md) for more tips.**
**Create your first notebook.** After [installing
marimo](../getting_started/installation.md), create your first notebook with
```bash
marimo edit my_notebook.py
```
at the command-line.
**The marimo library**.
We recommend starting each marimo notebook with a cell containing a single
line of code,
```python3
import marimo as mo
```
The marimo library lets you use interactive UI elements, layout elements,
dynamic markdown, and more in your marimo notebooks.
### How marimo executes cells
A marimo notebook is made of small blocks of Python code called **cells**.
_When you run a cell, marimo automatically runs all cells that read any global
variables defined by that cell._ This is reactive execution.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="600px" align="center">
<source src="/_static/reactive.mp4" type="video/mp4">
<source src="/_static/reactive.webm" type="video/webm">
</video>
</figure>
</div>
**Execution order.**
The order of cells on the page has no bearing on the order cells are
executed in: execution order is determined by the variables
cells define and the variables they read.
You have full freedom over how to organize your code and tell your stories:
move helper functions and other "appendices" to the bottom of your notebook, or
put cells with important outputs at the top.
**No hidden state.**
marimo notebooks have no hidden state because the program state is
automatically synchronized with your code changes and UI interactions. And if
you delete a cell, marimo automatically deletes that cell's variables,
preventing painful bugs that arise in traditional notebooks.
**No magical syntax.**
There's no magical syntax or API required to opt-in to reactivity: cells are
Python and _only Python_. Behind-the-scenes, marimo statically analyzes each
cell's code just once, creating a directed acyclic graph based on the
global names each cell defines and reads. This is how data flows
in a marimo notebook.
!!! warning "Minimize variable mutation."
marimo's understanding of your code is based on variable definitions and
references; marimo does not track mutations to objects at runtime. For this
reason, if you need to mutate a variable (such as adding a new column to a
dataframe), you should perform the mutation in the same cell as the one that
defines it.
Learn more in our [reactivity guide](../guides/reactivity.md#reactivity-mutations).
For more on reactive execution, open the dataflow tutorial
```bash
marimo tutorial dataflow
```
or read the [reactivity guide](../guides/reactivity.md).
### Visualizing outputs
marimo visualizes the last expression of each cell as its **output**. Outputs
can be any Python value, including markdown and interactive elements created
with the marimo library, (_e.g._, [`mo.md`][marimo.md], [`mo.ui.slider`][marimo.ui.slider]).
You can even interpolate Python values into markdown (using `mo.md(f"...")`) and
other marimo elements to build rich composite outputs:
<div align="center">
<figure>
<video autoplay muted loop playsinline width="600px" align="center">
<source src="/_static/outputs.mp4" type="video/mp4">
<source src="/_static/outputs.webm" type="video/webm">
</video>
</figure>
</div>
> Thanks to reactive execution, running a cell refreshes all the relevant outputs in your notebook.
The marimo library also comes with elements for laying out outputs, including
[`mo.hstack`][marimo.hstack], [`mo.vstack`][marimo.vstack],
[`mo.accordion`][marimo.accordion], [`mo.ui.tabs`][marimo.ui.tabs], [`mo.sidebar`][marimo.sidebar],
[`mo.nav_menu`][marimo.nav_menu], [`mo.ui.table`][marimo.ui.table],
and [many more](../api/layouts/index.md).
For more on outputs, try these tutorials:
```bash
marimo tutorial markdown
marimo tutorial plots
marimo tutorial layout
```
or read the [visualizing outputs guide](../guides/outputs.md).
### Creating interactive elements
The marimo library comes with many interactive stateful elements in
[`marimo.ui`](../api/inputs/index.md), including simple ones like sliders, dropdowns, text fields, and file
upload areas, as well as composite ones like forms, arrays, and dictionaries
that can wrap other UI elements.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="600px" align="center" src="/_static/readme-ui.webm">
</video>
</figure>
</div>
**Using UI elements.**
To use a UI element, create it with `mo.ui` and **assign it to a global
variable.** When you interact with a UI element in your browser (_e.g._,
sliding a slider), _marimo sends the new value back to Python and reactively
runs all cells that use the element_, which you can access via its `value`
attribute.
> **This combination of interactivity and reactivity is very powerful**: use it
> to make your data tangible during exploration and to build all kinds of tools
> and apps.
_marimo can only synchronize UI elements that are assigned to
global variables._ Use composite elements like [`mo.ui.array`][marimo.ui.array] and
[`mo.ui.dictionary`][marimo.ui.dictionary] if the set of UI elements is not
known until runtime.
!!! tip "Using buttons to execute cells"
Use [`mo.ui.run_button`][marimo.ui.run_button] to create a button that
triggers computation when clicked; see our recipes for [an example](../recipes.md#create-a-button-that-triggers-computation-when-clicked).
For more on interactive elements, run the UI tutorial
```bash
marimo tutorial ui
```
or read the [interactivity guide](../guides/interactivity.md).
### Querying dataframes and databases with SQL
marimo has built-in support for SQL: you can query Python dataframes,
databases, CSVs, Google Sheets, or anything else. After executing your query,
marimo returns the result to you as a dataframe, making it seamless
to go back and forth between SQL and Python.
<div align="center">
<figure>
<img src="/_static/docs-sql-df.png"/>
<figcaption>Query a dataframe using SQL!</figcaption>
</figure>
</div>
To create a SQL cell, click on the SQL button that appears at the bottom of the
cell array, or right click the create cell button next to a cell. Today,
SQL in marimo is executed using [duckdb](https://duckdb.org/docs/).
To learn more, run the SQL tutorial
```bash
marimo tutorial sql
```
or read the [SQL guide](../guides/working_with_data/sql.md).
## Running notebooks as applications
You can use marimo as a notebook, similar to how you might use Jupyter.
But you can also do more: because marimo notebooks are reactive and can include
interactive elements, hiding notebook code gives you a simple web app!
You can run your notebook as a read-only web app from the command-line:
```bash
marimo run my_notebook.py
```
The default renderer just hides the notebook code and concatenates outputs
vertically. But marimo also supports [other layouts](../guides/apps.md),
such as slides and grid.
## Running notebooks as scripts
Because marimo notebooks are stored as pure Python files, each notebook
can be executed as a script from the command-line:
```python
python my_notebook.py
```
You can also [pass command-line arguments](../guides/scripts.md) to scripts.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/coming_from/index.md
```md
# Coming from other tools
marimo is a **single tool** that replaces `jupyter`, `streamlit`, `jupytext`,
`ipywidgets`, `papermill`, and more.
| Guide | Description |
|-------|-------------|
| [Streamlit](streamlit.md) | Transitioning from Streamlit |
| [Jupytext](jupytext.md) | Transitioning from Jupytext |
| [Papermill](papermill.md) | Transitioning from Papermill |
!!! important "Coming from Jupyter?"
See our [coming from Jupyter guide](../coming_from/jupyter.md).
```
File: /Users/morganmcguire/ML/marimo/docs/getting_started/installation.md
```md
# Installation
Before installing marimo, we recommend creating and activating a Python
[virtual environment](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments).
??? note "Setting up a virtual environment"
Python uses virtual environments to minimize conflicts among packages.
Here's a quickstart for `pip` users. If you use `conda`, please use a [`conda`
environment](https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#creating-an-environment-with-commands)
instead.
Run the following in the terminal:
- create an environment with `python -m venv marimo-env`
- activate the environment:
- macOS/Unix: `source marimo-env/bin/activate`
- Windows: `marimo-env\Scripts\activate`
_Make sure the environment is activated before installing marimo and when
using marimo._ Install other packages you may need, such as numpy, pandas, matplotlib,
and altair, in this environment. When you're done, deactivate the environment
with `deactivate` in the terminal.
Learn more from the [official Python tutorial](https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments).
/// admonition | Using uv?
type: tip
[uv](https://github.com/astral-sh/uv) is a next-generation Python package
installer and manager that is 10-100x faster than pip, and also makes it easy
to install Python and manage projects. With `uv`, creating a virtual
environment is as easy as `uv venv`.
///
To install marimo, run the following in a terminal:
/// tab | install with pip
```bash
pip install marimo
```
///
/// tab | install with uv
```bash
uv pip install marimo
```
///
/// tab | install with conda
```bash
conda install -c conda-forge marimo
```
///
To check if the install worked, run
```bash
marimo tutorial intro
```
A tutorial notebook should open in your browser.
/// admonition | Installation issues?
type: note
Having installation issues? Reach out to us [at GitHub](https://github.com/marimo-team/marimo/issues) or [on Discord](https://marimo.io/discord?ref=docs).
///
```
File: /Users/morganmcguire/ML/marimo/docs/guides/coming_from/jupytext.md
```md
# Coming from Jupytext
If you're familiar with Jupytext, you'll find that marimo offers similar
functionality for working with notebooks as Python files, but without the need
for additional setup or synchronization issues because marimo notebooks
are stored as `.py` files by default. However, Jupytext works with IPython
notebooks, whereas marimo works with marimo notebooks, which are not based
on IPython/Jupyter. Here's a comparison to help you transition smoothly.
## Notebook Format
| Jupytext | marimo |
|----------|--------|
| Jupytext uses comments or special markers to define cell types in notebooks. | Notebooks are pure Python (`.py`) files by default, using standard Python syntax, such as decorators and functions, to define cells. |
## Converting Jupyter notebooks
### From `.ipynb`
| Jupytext | marimo |
|----------|--------|
| `jupytext --to py notebook.ipynb` | `marimo convert notebook.ipynb > notebook.py` |
!!! tip "From py:percent notebooks to marimo notebooks"
If you have a Python file encoded in the [py:percent](https://jupytext.readthedocs.io/en/latest/#text-notebooks)
format, you can convert it to a marimo notebook in two steps:
```
jupytext --to notebook.ipynb percent_notebook.py
marimo convert notebook.ipynb > marimo_notebook.py
```
### To `.ipynb`
| Jupytext | marimo |
|----------|--------|
| `jupytext --to notebook.ipynb notebook.py` | `marimo export ipynb notebook.py > notebook.ipynb` |
## Editing Notebooks
| Jupytext | marimo |
|----------|--------|
| Requires synchronization between `.ipynb` and `.py` files. | Edit marimo notebooks files directly in the marimo editor (`marimo edit notebook.py`), and changes are read from and written to the same file. |
## Executing Notebooks
| Jupytext | marimo |
|----------|--------|
| Use Jupyter to edit notebooks interactively, or Papermill to execute notebooks from the command line. | In addition to running notebooks interactively (`marimo notebook.py`), you can run notebooks as scripts (`python notebook.py`) or as apps (`marimo run notebook.py`), passing values to them with marimo's built-in support for [CLI args](../../api/cli_args.md). |
## Version Control
| Jupytext | marimo |
|----------|--------|
| Jupyter notebooks are stored as JSON by default, making them difficult to meaningfully version with git. Use Jupytext to pair and synchronize jupyter notebooks with text representations for smaller git diffs. | Notebooks are already in `.py` format, making them git-friendly by default. Small changes to the notebook are guaranteed to yield small diffs. |
## Markdown and Code Cells
| Jupytext | marimo |
|----------|--------|
| Uses special markers or formats to distinguish cell types. Magical syntax is required. | Uses `mo.md("...")` for Markdown content, and interpolate Python values with `mo.md(f"...")`; no magical syntax. |
## Deployment
| Jupytext | marimo |
|----------|--------|
| Requires migrating to other libraries like Voila or Streamlit for deployment. | Can be deployed as interactive web apps with `marimo run`. |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/coming_from/jupyter.md
```md
# Coming from Jupyter
If you're coming from Jupyter, here are a few tips to help you adapt to marimo
notebooks.
## Adapting to marimo's execution model
The biggest difference between marimo and Jupyter is the execution model.
A **Jupyter** notebook is a **REPL**: you execute blocks of code one at a time,
and Jupyter has no understanding of how different blocks are related to each
other. As a result a Jupyter notebook can easily
accumulate "hidden state" (and hidden bugs) --- you might accidentally execute
cells out of order, or you might run (or delete) a cell but forget to re-run
cells that depended on its variables. Because of this, Jupyter notebooks
suffer from a [reproducibility crisis](../../faq.md#faq-problems), with over
a third of Jupyter notebooks on GitHub failing to reproduce.
Unlike Jupyter, **marimo** notebooks understand how different blocks of
code are related to each other, modeling your code as a graph on cells
based on variable declarations and references. This eliminates hidden
state, and it's also what enables marimo notebooks to be reused as
apps and scripts.
**By default, if you run a cell in marimo, all other cells that read its
variables run automatically.** While this ensures that your code and outputs are
in sync, it can take some time getting used to. **Here are some tips and tools to
help you adapt to marimo's execution model.**
### Configure marimo's runtime
[Configure marimo's runtime](../configuration/runtime_configuration.md) to
not autorun on startup or on cell execution.
Even when autorun is disabled, marimo still tracks dependencies across cells,
marking dependents of a cell as stale when you run it. You can click a single
button to run all your stale cells and bring your notebook back up-to-date.
### Stop execution with `mo.stop`
Use [`mo.stop`][marimo.stop] to stop a cell from executing if a condition
is met:
```python
# if condition is True, the cell will stop executing after mo.stop() returns
mo.stop(condition)
# this won't be called if condition is True
expensive_function_call()
```
Use [`mo.stop()`][marimo.stop] in conjunction with
[`mo.ui.run_button()`][marimo.ui.run_button] to require a button press for
expensive cells:
/// marimo-embed
size: medium
```python
@app.cell
def __():
run_button = mo.ui.run_button()
run_button
return
@app.cell
def __():
mo.stop(not run_button.value, mo.md("Click 👆 to run this cell"))
mo.md("You clicked the button! 🎉")
return
```
///
### Working with expensive notebooks
For more tips on adapting to marimo's execution model, see our guide
on [working with expensive notebooks](../expensive_notebooks.md).
## Adapting to marimo's restriction on redefining variables
marimo "compiles" your notebook cells into a directed graph on cells,
linked by variable declarations and references, reusing this graph to
run your notebook as a script or app. For marimo's compilation to work,
the same variable cannot be defined in multiple cells; otherwise, marimo
wouldn't know what order to run cells in.
To adapt to the restriction, we suggest:
1. Encapsulating code into functions when possible, to minimize the number
of global variables.
2. Prefixing temporary variables with an underscore (`_my_temporary`), which
makes the variable **local** to a cell.
3. Mutating variables in the cell that defines them.
When working with **dataframes**, you might be used to redefining the same `df`
variable in multiple cells. That won't work in marimo. Instead, try merging
the cells into a single cell:
_Don't_ do this:
```python
df = pd.DataFrame({"my_column": [1, 2]})
```
```python
df["another_column"] = [3, 4]
```
_Instead_, do this:
```python
df = pd.DataFrame({"my_column": [1, 2]})
df["another_column"] = [3, 4]
```
If you do need to transform a dataframe across multiple cells, you can
alias the dataframe:
```python
df = pd.DataFrame({"my_column": [1, 2]})
```
```python
augmented_df = df
augmented_df["another_column"] = [3, 4]
```
## Adapting to marimo's file format
marimo stores notebooks as Python, not JSON. This lets you version notebooks
with git, [execute them as scripts](../scripts.md), and import named
cells into other Python files. However, it does mean that your notebook outputs
(e.g., plots) are not stored in the file.
If you'd like to keep a visual record of your notebook work, [enable
the "Auto-download as HTML" setting](../configuration/index.md), which will
periodically snapshot your notebook as HTML to a `__marimo__` folder in the
notebook directory.
### Converting Jupyter notebooks to marimo notebooks
Convert Jupyter notebooks to marimo notebooks at the command-line:
```bash
marimo convert your_notebook.ipynb -o your_notebook.py
```
## Adapting to the absence of magic commands
Because marimo notebooks are just Python (improving maintainability), marimo
doesn't support IPython magic commands or `!`-prefixed console commands. Here
are some alternatives.
### Run console commands with subprocess.run
To run a console command, use Python's [subprocess.run](https://docs.python.org/3/library/subprocess.html#subprocess.run):
```python
import subprocess
# run: "ls -l"
subprocess.run(["ls", "-l"])
```
### Common magic commands replacements
| Magic Command | Replacement |
| ------------- | ---------------------------------------------------------------------------------------------- |
| %cd | `os.chdir()`, see also [`mo.notebook_dir()`][marimo.notebook_dir] |
| %clear | Right-click or toggle the cell actions |
| %debug | Python's built-in debugger: `breakpoint()` |
| %env | `os.environ` |
| %load | N/A - use Python imports |
| %load_ext | N/A |
| %autoreload | marimo's [module autoreloader](../editor_features/module_autoreloading.md) |
| %matplotlib | marimo auto-displays plots |
| %pwd | `os.getcwd()` |
| %pip | Use marimo's [built-in package management](../editor_features/package_management.md) |
| %who_ls | `dir()`, `globals()`, [`mo.refs()`][marimo.refs], [`mo.defs()`][marimo.defs] |
| %system | `subprocess.run()` |
| %%time | `time.perf_counter()` or Python's timeit module |
| %%timeit | Python's timeit module |
| %%writefile | `with open("file.txt", "w") as f: f.write()` |
| %%capture | [`mo.capture_stdout()`][marimo.capture_stdout], [`mo.capture_stderr()`][marimo.capture_stderr] |
| %%html | [`mo.Html()`][marimo.Html] or [`mo.md()`][marimo.md] |
| %%latex | [`mo.md(r'$$...$$')`][marimo.md] |
### Installing packages with marimo's package manager
Use marimo's package management sidebar panel to install packages to your current
environment. Learn more in our [package management
guide](../editor_features/package_management.md).
## Interactive guide
This guide contains additional tips to help you adapt to marimo. Fun fact: the
guide is itself a marimo notebook!
<iframe src="https://marimo.app/l/z0aerp?embed=true" class="demo xxlarge" frameBorder="0">
</iframe>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/coming_from/papermill.md
```md
# Coming from Papermill
marimo provides built-in support for parametrizing and executing marimo
notebooks. If you're familiar with Papermill, this guide will help you
understand how to achieve similar functionality using marimo's features.
## Parameterizing Notebooks
**Papermill**
Papermill allows you to parameterize Jupyter notebooks by defining a "parameters" cell
and injecting values at runtime.
**marimo**
marimo offers two main ways to parameterize notebooks:
1. **Command Line Arguments**:
Use [`mo.cli_args`](../../api/cli_args.md) to access command-line arguments passed to your notebook.
```python
import marimo as mo
# Access CLI args
args = mo.cli_args()
param1 = args.get("param1", "default_value")
```
Run your notebook as a script with:
```bash
python notebook.py -- --param1 value1
```
Run your notebook as an app with:
```bash
marimo run notebook.py -- --param1 value1
```
2. **Query Parameters**:
For web apps, use `mo.query_params` to access URL query parameters.
```python
import marimo as mo
# Access query params
params = mo.query_params()
param1 = params.get("param1", "default_value")
```
Access your app with:
```bash
marimo run notebook.py
```
Then visit:
```bash
http://your-app-url/?param1=value1
```
## Executing Notebooks
**Papermill**
Papermill allows you to execute notebooks programmatically and pass parameters.
**marimo**
marimo notebooks are pure Python files, making them easy to execute
programmatically.
1. **Running a named cell**:
After naming a cell in your file, you can run it using the
[cell execution API][marimo.Cell.run].
```python
from my_notebook import my_cell
# last_expression is the visual output of the cell
# definitions is a dictionary of the variables defined by the cell
last_expression, definitions = my_cell.run()
```
This API also allows for parametrizing the inputs to the cell; to learn more,
make sure to checkout [the example][marimo.Cell.run] in our API reference.
2. **Using subprocess**:
```python
import subprocess
subprocess.run(["python", "notebook.py", "--", "--param1", "value1"])
```
## Storing or Sharing Artifacts
**Papermill**
Papermill can store executed notebooks with output.
**marimo**
marimo offers several options for storing and sharing outputs:
1. **Export to HTML**:
```bash
marimo export html notebook.py -o notebook.html -- -arg1 foo --arg2 bar
```
2. **Deploy as Web App**:
```bash
marimo run notebook.py
```
3. **Auto-export HTML**:
You can configure marimo to automatically export to HTML during the editing process.
This is configured in the marimo application settings directly in the editor.
This way, after changes are made to your notebook, an HTML snapshot is generated,
and placed in a `.marimo/` directory in the same location as your notebook.
## Workflow Integration
**Papermill**
Papermill is often used in data pipelines and workflow systems.
**marimo**
marimo notebooks can be easily integrated into workflows:
1. **As Python Scripts**:
marimo notebooks are Python files, so they can be executed directly in most workflow systems.
See [our examples](https://github.com/marimo-team/marimo/tree/main/examples) for integrating with
popular tools.
2. **Programmatic Execution**:
Importing notebook as Python modules or executing via subprocess allows for chaining together multiple notebooks in a workflow.
```
File: /Users/morganmcguire/ML/marimo/docs/getting_started/quickstart.md
```md
# Quickstart
Installing marimo gets you the `marimo` command-line interface (CLI), the entry
point to all things marimo.
## Run tutorials
`marimo tutorial intro` opens the intro tutorial. List all tutorials with
```bash
marimo tutorial --help
```
## Edit notebooks
Create and edit notebooks with `marimo edit`.
- launch the notebook server to create new notebooks,
and start or stop existing ones:
```bash
marimo edit
```
- create or edit a single notebook with
```bash
marimo edit your_notebook.py
```
(If `your_notebook.py` doesn't exist, marimo will create a blank notebook
named `your_notebook.py`.)
## Deploy as apps
Use `marimo run` to serve your notebook as an app, with Python code hidden and
uneditable.
```bash
marimo run your_notebook.py
```
## Convert from Jupyter to marimo
Automatically convert Jupyter notebooks to marimo notebooks with `marimo convert`:
```bash
marimo convert your_notebook.ipynb -o your_notebook.py
```
Then open the notebook with `marimo edit your_notebook.py`
!!! tip "Disable autorun on startup"
marimo automatically runs notebooks when they are opened. If this
is a problem for you (not all Jupyter notebooks are designed to be run on
startup), you can disable autorun on startup via [user configuration](../guides/configuration/runtime_configuration.md).
1. Type `marimo config show` to get the location of your config file.
2. If no config file exists, create it at `~/.marimo.toml` or `$XDG_CONFIG_HOME/marimo/marimo.toml`.
3. Update your config to include the following:
```toml title="marimo.toml"
[runtime]
auto_instantiate = false
```
## Export marimo notebooks to other file formats
Use
```bash
marimo export
```
to convert marimo notebooks to other file formats, including HTML, IPYNB,
and markdown.
## Install optional dependencies for more features
Some features require additional dependencies, which are not installed by default. This includes:
- [SQL cells](../guides/working_with_data/sql.md)
- Charts in the datasource viewer
- [AI features](../guides/editor_features/ai_completion.md)
- Format on save
To install the optional dependencies, run:
/// tab | install with pip
```bash
pip install "marimo[recommended]"
```
///
/// tab | install with uv
```bash
uv pip install "marimo[recommended]"
```
///
/// tab | install with conda
```bash
conda install -c conda-forge marimo duckdb altair polars openai ruff
```
///
This will install: `duckdb`, `altair`, `polars`, `openai`, and `ruff`.
## Enable GitHub Copilot and AI Assistant
The marimo editor natively supports [GitHub Copilot](https://copilot.github.com/),
an AI pair programmer, similar to VS Code.
_Get started with Copilot_:
1. Install [Node.js](https://nodejs.org/en/download).
2. Enable Copilot via the settings menu in the marimo editor.
_Note_: Copilot is not yet available in our conda distribution; please install
marimo from `PyPI` if you need Copilot.
marimo also comes with support for [other copilots](../guides/editor_features/ai_completion.md#codeium-copilot),
and a built-in [AI assistant](../guides/editor_features/ai_completion.md#generate-code-with-our-ai-assistant) that helps you write code.
## Try our VS Code extension
The best way to use marimo is through the CLI. However, if you prefer VS Code
over terminal, try our [VS Code
extension](https://marketplace.visualstudio.com/items?itemName=marimo-team.vscode-marimo).
Use this extension to edit and run notebooks directly from VS Code, and to list
all marimo notebooks in your current directory.
<div align="center">
<figure>
<img src="/_static/vscode-marimo.png" alt="VS Code extension for marimo"/>
</figure>
</div>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/coming_from/streamlit.md
```md
# Coming from Streamlit
If you're familiar with Streamlit and looking to transition to marimo, read on.
The biggest difference between Streamlit and marimo is that
Streamlit can only be used for data apps, whereas marimo is a notebook-first
programming environment that makes it effortless to run notebooks as apps.
In addition, marimo is much more performant than streamlit.
## Key Differences
1. **Notebook vs. App Framework**:
- marimo is primarily a reactive notebook
environment, while Streamlit is an app framework.
- marimo notebooks can be run as apps -- often with better performance
than streamlit apps -- but they're designed with a notebook-first approach.
- When creating streamlit apps, it is common to first prototype them as Jupyter
notebooks, then migrate and refactor them into streamlit apps. With marimo,
every notebook is automatically an app; there's no migration step needed.
2. **Performance.**
- marimo uses a reactive execution model that, on interaction or code
change, runs the minimal subset of notebook code needed to keep your
notebook up-to-date.
- Streamlit reruns the entire script on each interaction, which frequently
causes performance issues.
3. **File Format**:
- marimo notebooks and Streamlit apps are pure Python files (.py).
- marimo's structure allows for more fine-grained reactivity.
- Unlike streamlit files, marimo files can be executed as Python scripts from the
command-line, and can be imported and used as a module by other Python
programs. For example, other programs can [reuse cells][marimo.Cell.run] from
a marimo notebook.
4. **UI Elements**:
- Both offer UI elements like sliders, text fields, and tables.
- In streamlit,
creating a UI element automatically outputs it to the display.
-In marimo, the
creation of a UI element is separated from its display, meaning that you can
easily create custom layouts and higher-order elements, and even emit the same UI element twice.
- marimo support the [anywidget](https://anywidget.dev/) spec for custom UI components, letting
you reuse widgets that were originally developed for the Jupyter ecosystem,
- streamlit has its own system for creating custom components.
5. **Built-in Editor**:
- marimo includes a [built-in editor](../editor_features/index.md) for notebooks, designed specifically
for working with data.
- Streamlit relies on external editors.
- Both approaches have their pros and cons.
6. **Working with data.**:
- marimo's notebook environment allows for iterative and interactive
development and exploration, letting it serve as your daily driver for
working with data. marimo even has native support for [SQL](../working_with_data/sql.md).
- Streamlit is exclusively used for building standalone data apps.
## Common Streamlit Features in marimo
### 1. Displaying text
Streamlit:
```python
import streamlit as st
st.markdown(
"""
# Greetings
Hello world
"""
)
```
marimo:
```python
import marimo as mo
mo.md(
"""
# Greetings
Hello world
"""
)
```
### 2. Displaying Data
Streamlit:
```python
st.dataframe(df)
```
marimo:
```python
df # Last expression in a cell is automatically displayed
```
### 3. Input Widgets
Streamlit:
```python
age = st.slider("How old are you?", 0, 130, 25)
```
marimo:
```python
age = mo.ui.slider(label="How old are you?", start=0, stop=130, value=25)
mo.md(f"One more question: {age}") # marimo can achieve more advanced composition
```
### 4. Buttons
Streamlit:
```python
if st.button("Click me"):
st.write("Button clicked!")
```
marimo:
```python
button = mo.ui.run_button("Click me")
```
```python
# In another cell
if button.value:
mo.output.replace(mo.md("Button clicked!"))
```
```
# Or
mo.md("Button clicked!") if button.value else None
```
### 5. Layouts
Streamlit:
```python
col1, col2 = st.columns(2)
with col1:
st.write("Column 1")
with col2:
st.write("Column 2")
```
marimo:
```python
mo.hstack([
mo.md("Column 1"),
mo.md("Column 2")
])
```
### 6. Advanced Layouts (tabs, accordions)
Streamlit:
```python
with st.expander("Expand me"):
st.write("Hello from the expander!")
```
marimo:
```python
mo.accordion({"Expand me": "Hello from the expander!"})
```
marimo's unique approach to composition allows for more flexible layouts with
unlimited nesting.
### 6. Plotting
Streamlit:
```python
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([1, 2, 3, 4])
st.pyplot(fig)
```
marimo:
```python
import matplotlib.pyplot as plt
plt.plot([1, 2, 3, 4])
plt.gca() # Last expression is displayed
```
### 7. Caching
Streamlit:
```python
@st.cache_data
def expensive_computation(args):
# ...
```
marimo:
```python
@functools.cache
def expensive_computation(args):
# ...
```
### 8. Session State
Streamlit uses `st.session_state` for persisting data. In marimo, you can use
regular Python variables, as the notebook maintains consistent state for cells
that are not re-executed.
### 9. Running as an App
Streamlit:
```bash
streamlit run your_app.py
```
marimo:
```bash
marimo run your_notebook.py
```
## Key Concepts to Remember
1. In marimo, cells are automatically re-executed when their dependencies change. But only the affected cells are re-executed, making it far more efficient than a naively written streamlit program.
2. UI elements in marimo are typically assigned to variables and their values accessed via the `value` attribute.
3. marimo's `mo.md()` function is versatile and can include both text and UI elements with f-strings.
4. marimo's notebook-first approach allows it to be used for all kinds of data work, including exploratory data analysis, data engineering, machine learning experimentation and model training, library documentation and examples, and more.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/configuration/index.md
```md
# Configuration
marimo offers two types of configuration: User Configuration and App
Configuration. Both can be easily managed through the Settings menu in the
marimo editor.
<img align="right" src="/_static/docs-app-config.png" width="300px"/>
## App Configuration
App Configuration is specific to each notebook and is stored in the `notebook.py` file. This allows you to customize various aspects of your notebook, including:
- Notebook width
- Notebook title
- [Custom CSS](theming.md)
- [Custom HTML Head](html_head.md)
- Automatically download HTML snapshots
Configure these settings through the notebook menu (⚙️) in the top-right corner.
<br clear="left"/>
## User Configuration
User Configuration applies globally across all marimo notebooks and is stored
in a `.marimo.toml` file.
While you can edit the `.marimo.toml` file directly, we recommend using the
marimo UI for a more user-friendly experience.
<video controls width="100%" height="100%" align="center" src="/_static/docs-user-config.mp4"> </video>
You can customize the following settings:
- [Runtime](runtime_configuration.md), including whether notebooks autorun
- [Hotkeys](../editor_features/hotkeys.md)
- Completion (auto-completion, AI copilot, etc.)
- Display (theme, font size, output placement, etc.)
- Autosave
- [Package management](../editor_features/package_management.md#package-management)
- Server settings
- [VIM keybindings](../editor_features/overview.md#vim-keybindings)
- Formatting settings
- [AI assistance](../editor_features/ai_completion.md)
- Experimental features
### User configuration file
marimo searches for the `.marimo.toml` file in the following order:
1. Current directory
2. Parent directories (moving up the tree)
3. Home directory (`~/.marimo.toml`)
4. [XDG](https://xdgbasedirectoryspecification.com/) directory (`~/.config/marimo/marimo.toml` or `$XDG_CONFIG_HOME/marimo/marimo.toml`)
If no `.marimo.toml` file is found, marimo creates one for you in an XDG config
compliant way.
To view your current configuration and locate the config file, run:
```bash
marimo config show
```
To describe the user configuration options, run:
```bash
marimo config describe
```
### Overriding settings with pyproject.toml
You can override user configuration settings with a `pyproject.toml` file. This
is useful for sharing configurations across teams or ensuring consistency across
notebooks. You must edit the `pyproject.toml` file directly to override settings.
For example, the following `pyproject.toml` file overrides the `autosave` setting
in the user configuration:
```toml title="pyproject.toml"
[tool.marimo.format]
line_length = 120
[tool.marimo.display]
default_width = "full"
```
You can override any user configuration setting in this way. To find these settings run `marimo config show`.
## Environment Variables
marimo supports the following environment variables for advanced configuration:
| Environment Variable | Description | Default Value |
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------- |
| `MARIMO_OUTPUT_MAX_BYTES` | Maximum size of output that marimo will display. Outputs larger than this will be truncated. | 5,000,000 (5MB) |
| `MARIMO_STD_STREAM_MAX_BYTES` | Maximum size of standard stream (stdout/stderr) output that marimo will display. Outputs larger than this will be truncated. | 1,000,000 (1MB) |
| `MARIMO_SKIP_UPDATE_CHECK` | If set to "1", marimo will skip checking for updates when starting. | Not set |
| `MARIMO_SQL_DEFAULT_LIMIT` | Default limit for SQL query results. If not set, no limit is applied. | Not set |
### Tips
- The `.marimo.toml` file can be version controlled to share configurations across teams
- App configurations can be committed with notebooks to ensure consistent appearance
```
File: /Users/morganmcguire/ML/marimo/docs/guides/configuration/runtime_configuration.md
```md
# Runtime configuration
Through the notebook settings menu, you can configure how and when marimo
runs cells.
<video controls width="100%" height="100%" align="center" src="/_static/docs-runtime-config.mp4"> </video>
## On startup
By default, marimo notebooks run automatically on startup; just how the command
```bash
python main.py
```
executes a script,
```bash
marimo edit notebook.py
```
executes the notebook.
Disable this behavior by unchecking "Autorun on startup".
_When sharing a notebook as an app with `marimo run`, this setting has
no effect._
## On cell change
By default, when a cell is run or a UI element is interacted with, marimo
automatically runs cells that reference any of its variables. **You can disable
automatic execution of cell's descendants in the notebook settings menu by
setting `"On cell change"` to `"lazy"`.**
When the runtime is lazy, running a cell marks affected cells as stale but
doesn't automatically run them. Lazy evaluation means cells are only run when
their outputs are needed. If you run a cell that has stale ancestors, those
ancestors will also run to make sure your cell doesn't use stale inputs. You
can always click the notebook run button or use the keyboard shortcut to run
all stale cells.
**When should I use lazy evaluation?** Choosing the lazy runtime can be helpful
when working on notebooks with expensive cells.
!!! tip "Tip: speed up expensive notebooks with marimo's smart caching"
In addition to runtime configuration, marimo also provides [opt-in caching](../../api/caching.md)
to help you work with expensive or side-effectful notebooks. marimo's
can cache expensive functions in memory and expensive blocks of code to disk,
letting you skip entire sections of your code and automatically loading
variables in memory on notebook startup. Read our [caching
guide](../../api/caching.md) to learn more.
_When sharing a notebook as an app with `marimo run`, this setting has
no effect._
## On module change
When module autoreloading is enabled, marimo automatically runs cells when you
edit Python files. Based on static analysis, the reloader only runs cells
affected by your edits. The reloader is recursive, meaning that marimo tracks
modifications for modules imported by your notebook's imported modules too.
!!! tip "Why autoreload?"
Autoreloading enables a workflow that many developers find
productive: develop complex logic in Python modules, and use the marimo
notebook as a DAG or main script that orchestrates your logic.
Autoreloading comes in two types:
1. **autorun**: automatically re-runs cells affected by module modification.
<figure>
<video controls loop width="100%" height="100%" align="center" src="/_static/docs-module-reloading.mp4"> </video>
<figcaption align="center">When set to autorun, marimo's reloader automatically run cells when you edit Python files.</figcaption>
</figure>
2. **lazy**: marks cells affected by module modifications as stale, letting you know which cells need to be re-run.
<figure>
<video controls loop width="100%" height="100%" align="center" src="/_static/docs-module-reloading-lazy.mp4"> </video>
<figcaption align="center">When set to lazy, marimo's reloader marks cells as stale when you edit Python files.</figcaption>
</figure>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/configuration/html_head.md
```md
# Custom HTML Head
You can include a custom HTML head file to add additional functionality to your notebook, such as analytics, custom fonts, meta tags, or external scripts. The contents of this file will be injected into the `<head>` section of your notebook.
To include a custom HTML head file, specify the relative file path in your app configuration. This can be done through the marimo editor UI in the notebook settings (top-right corner).
This will be reflected in your notebook file:
```python
app = marimo.App(html_head_file="head.html")
```
## Example Use Cases
Here are some common use cases for custom HTML head content:
1. **Google Analytics**
```html
<!-- head.html -->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-XXXXXXXXXX');
</script>
```
2. **Custom Fonts**
```html
<!-- head.html -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet" />
```
3. **Meta Tags**
```html
<!-- head.html -->
<meta name="description" content="My marimo notebook" />
<meta name="keywords" content="data science, visualization, python" />
<meta name="author" content="Your Name" />
<meta property="og:title" content="My Notebook" />
<meta property="og:description" content="Interactive data analysis with marimo" />
<meta property="og:image" content="https://example.com/thumbnail.jpg" />
```
4. **External Scripts and Libraries**
```html
<!-- head.html -->
<!-- Load external JavaScript libraries -->
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
<!-- Load external CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" />
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/authentication.md
```md
# Authentication
marimo provides a simple way to add token/password protection to your marimo server. Given that authentication is a complex topic, marimo does not provide a built-in authentication/authorization system, but instead makes it easy to add your own through ASGI middleware.
## Enabling Basic Authentication
Authentication is enabled by default when running `marimo edit/tutorial/new`. To disable authentication, you may pass `--no-token` to your `marimo edit/run/new` command from the Terminal. The auth token will be randomly generated when in `Edit mode` and deterministically generated in `Run mode` (based on the code of the notebook). However, you can also pass your own token/password using the `--token-password` flag.
```bash
marimo run my_notebook.py --token --token-password="sup3rs3cr3t"
```
### Ways to Authenticate
In order to authenticate, you must either pass the token as a password in the `Authorization` header, or as a query parameter under `access_token` in the URL.
1. Enter the token in the login page:
If you try to access marimo from a browser, you will be redirected to a login page where you can enter the token.
2. Query parameter:
To authenticate using a query parameter, you must pass the token as a query parameter under `access_token` in the URL. For example, to authenticate with the token `sup3rs3cr3t`, you would pass the query parameter `http://localhost:2718?access_token=sup3rs3cr3t`.
For convenience, when running locally, marimo will automatically open the URL with the query parameter in your default browser.
3. Basic Authorization header:
To authenticate using the `Authorization` header, you must pass the token as a password in the `Authorization` header using the [Basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). For example, to authenticate with the token `sup3rs3cr3t`, you would pass the header `Authorization Basic base64("any_username:sup3rs3cr3t")`.
This is not necessary when using a browser, as the marimo server will redirect you to a minimal login page where you can enter the token.
## Custom Authentication
If you choose to make your marimo application public, you may want to add your own authentication system, along with authorization, rate limiting, etc. You can do this by creating a marimo application programmatically and adding your own middleware to the ASGI application.
Here's an example of how you can add authentication to a marimo application using FastAPI:
```python
from typing import Annotated, Callable, Coroutine
from fastapi.responses import HTMLResponse, RedirectResponse
import marimo
from fastapi import FastAPI, Form, Request, Response
# Custom auth middleware and login page
from my_auth_module import auth_middleware, my_login_route
# Create a marimo asgi app
server = (
marimo.create_asgi_app()
.with_app(path="", root="./pages/index.py")
.with_app(path="/dashboard", root="./pages/dashboard.py")
.with_app(path="/sales", root="./pages/sales.py")
)
# Create a FastAPI app
app = FastAPI()
app.add_middleware(auth_middleware)
app.add_route("/login", my_login_route, methods=["POST"])
app.mount("/", server.build())
# Run the server
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
```
or for a full example on implementing OAuth2 with FastAPI, see the [FastAPI OAuth2 example](https://fastapi.tiangolo.com/tutorial/security/oauth2-jwt/).
```
File: /Users/morganmcguire/ML/marimo/docs/guides/configuration/theming.md
```md
# Theming
marimo provides basic support for theming. You can include a custom CSS file in your notebook that will be applied to the entire notebook. This allows you to customize the appearance of your notebook to your liking.
To include a custom CSS file, in the configuration dropdown, add the relative file path to your CSS file in the `Custom CSS` field. Once saved, you should see the changes applied to your notebook:
```python
app = marimo.App(css_file="custom.css")
```
## CSS Variables
We support only a few CSS variables as part of the "public API" for theming. These are:
```css
--marimo-monospace-font
--marimo-text-font
--marimo-heading-font
```
!!! warning "Other CSS Variables"
We cannot guarantee that other CSS variables or classnames will be stable across versions.
## Example
Here is an example of a custom CSS file that changes the font of the notebook:
```css
/* Load Inter from Google Fonts */
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
:root {
--marimo-heading-font: 'Inter', sans-serif;
}
/* Increase paragraph font size and change color */
.paragraph {
font-size: 1.2rem;
color: light-dark(navy, pink);
}
```
## Custom HTML Head
You can further customize your notebook by adding custom HTML in the `<head>` section of your notebook. This allows you to add additional functionality to your notebook, such as analytics, custom fonts, meta tags, or external scripts.
See the [Custom HTML Head](html_head.md) guide for more details.
## Targeting cells
You can target a cell's styles from the `data-cell-name` attribute. You can also target a cell's output with the `data-cell-role="output"` attribute.
```css
/* Target the cell named "My Cell" */
[data-cell-name="my_cell"] {
background-color: light-dark(navy, pink);
}
/* Target the output of the cell named "My Cell" */
[data-cell-name="my_cell"] [data-cell-role="output"] {
background-color: light-dark(navy, pink);
}
```
## Community Themes
The marimo community maintains a [library of custom themes](https://github.com/metaboulie/marimo-themes) that you can use in your notebooks. The library includes various themes like "coldme", "nord", "mininini", and "wigwam", each supporting both light and dark modes.
You can:
- Browse and download existing themes
- Use them in your own notebooks
- Contribute your own themes to share with the community
Visit the [marimo-themes repository](https://github.com/metaboulie/marimo-themes) to explore available themes and learn how to contribute your own.
## More customizations
We want to hear from you! If you have any suggestions for more customization options, please let us know on [GitHub](https://github.com/marimo-team/marimo/discussions)
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_docker.md
```md
# Deploy with Docker
## Prerequisites
- A marimo notebook or app: `app.py` that you want to deploy
- A `requirements.txt` file that contains the dependencies needed for your application to run
## Create a Dockerfile
`Dockerfile` is a text file that contains instructions for building a Docker image. Here's an example `Dockerfile` for a marimo notebook:
```Dockerfile
# syntax=docker/dockerfile:1.4
# Choose a python version that you know works with your application
FROM python:3.11-slim
# Install uv for fast package management
COPY --from=ghcr.io/astral-sh/uv:0.4.20 /uv /bin/uv
ENV UV_SYSTEM_PYTHON=1
WORKDIR /app
# Copy requirements file
COPY --link requirements.txt .
# Install the requirements using uv
RUN uv pip install -r requirements.txt
# Copy application files
COPY --link app.py .
# Uncomment the following line if you need to copy additional files
# COPY --link . .
EXPOSE 8080
# Create a non-root user and switch to it
RUN useradd -m app_user
USER app_user
CMD [ "marimo", "run", "app.py", "--host", "0.0.0.0", "-p", "8080" ]
```
## Breaking it down
`FROM` instructs what base image to choose. In our case, we chose Python 3.11 with the “slim” variant. This removes a lot of extra dependencies. You can always add them back as needed.
A slimmer Dockerfile (by bytes) means quick to build, deploy, and start up.
The `WORKDIR` sets the current working directory. In most cases, this does not need to be changed.
The `COPY` steps will copy all the necessary files into your docker. By adding `--link`, we end up creating a new layer that does not get invalidated by previous changes. This can be especially important for expensive install steps that do not depend on each other.
`RUN` lets us run shell commands. We can use this to install dependencies via apt-get, pip, or package managers. In our case, we use it to install our requirements.txt with pip.
Our `EXPOSE` step tells us which port is exposed to be accessed from outside the Docker container. This will need to match the port at which we run our marimo application on.
We then create a new user and switch to it with the `USER` instruction, in order to limit the permissions of the marimo application. This is not required, but recommended.
The final step `CMD` instructions what command to run when we run our docker container. Here we run our marimo application at the port 8080.
## Running your application locally
Once you have your Dockerfile and your application files, you can test it out locally:
```bash
# Build your image, and tag it as my_app
docker build -t my_app .
# Start your container, mapping port 8080
docker run -p 8080:8080 -it my_app
# Visit http://localhost:8080
```
After verifying that your application runs without errors, you can use these files to deploy your application on your preferred cloud provider that supports dockerized applications.
## Health checks
You can add a health check to your Dockerfile to ensure that your application is running as expected. This is especially useful when deploying to a cloud provider.
```Dockerfile
HEALTHCHECK --interval=30s --timeout=3s \
CMD curl -f http://localhost:8080/health || exit 1
```
The following endpoints may be useful when deploying your application:
- `/health` or `/healthz` - A health check endpoint that returns a 200 status code if the application is running as expected
- `/api/status` - A status endpoint that returns a JSON object with the status of the server
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_nginx.md
```md
# Deploy with nginx
nginx is a popular web server that can be used as a reverse proxy for web applications. This guide will show you how to deploy marimo behind an nginx reverse proxy.
## Prerequisites
- A marimo notebook or app that you want to deploy
- nginx installed on your server
- Basic understanding of nginx configuration
## Configuration
Create a new configuration file in `/etc/nginx/conf.d/` (e.g., `marimo.conf`):
```nginx
server {
server_name your-domain.com;
location / {
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:2718;
# Required for WebSocket support
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 600;
}
# Optional: Serve static files
location /static/ {
alias /path/to/your/static/files/;
}
}
```
## Breaking it down
- `server_name`: Replace with your domain name
- `proxy_pass`: Points to your marimo application (default port is 2718)
- WebSocket support: The following lines are required for marimo to function properly:
```nginx
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
```
- `proxy_read_timeout`: Increased to 600 seconds to handle long-running operations
## Running your application
1. Start your marimo application:
```bash
marimo run app.py --host 127.0.0.1 --port 2718
```
2. Test your nginx configuration:
```bash
nginx -t
```
3. Reload nginx to apply changes:
```bash
nginx -s reload
```
Your marimo application should now be accessible at your domain.
## SSL/HTTPS
For production deployments, it's recommended to use HTTPS. You can use [Certbot](https://certbot.eff.org/) to automatically configure SSL with Let's Encrypt certificates.
## Common Issues
### Kernel Not Found
If you see a "kernel not found" error, ensure that:
1. WebSocket support is properly configured in your nginx configuration
2. The proxy headers are correctly set
3. Your marimo application is running and accessible at the specified proxy_pass address
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_public_gallery.md
```md
# Deploy to our public gallery
If you would like to deploy your application to our [public
gallery](https://marimo.io/gallery), please reach out on
[Discord](https://marimo.io/discord?ref=docs).
You can also easily share your notebooks on the public web using [WASM
notebooks](../../guides/wasm.md), which run entirely in the browser, no backend
required.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_railway.md
```md
# Deploy to Railway
Railway is a platform that allows you to deploy Dockerize containers easily.
Using this pre-built template, Railway will deploy a single-instance marimo
edit server with persistent storage in a few clicks.
## Deploy
[![Deploy on Railway](https://railway.app/button.svg)](https://railway.app/template/iX6puU?referralCode=WdmHYp)
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_hugging_face.md
```md
# Deploy to Hugging Face
Hugging Face is a platform that allows you to deploy machine learning models and applications easily.
You can deploy a marimo notebook as an interactive web app on Hugging Face Spaces with just a few steps.
## Deploy
To deploy your marimo notebook to Hugging Face Spaces:
1. Create a new Space on Hugging Face by forking or copying the following template: <https://huggingface.co/spaces/marimo-team/marimo-app-template/tree/main>
2. Replace the contents of the `app.py` file with your marimo notebook.
3. Update the `requirements.txt` file to include any other dependencies your notebook requires.
4. Commit these files to your Space, and Hugging Face will automatically deploy your marimo notebook as an interactive web app.
For more detailed instructions and advanced configurations, please refer to the [Hugging Face Spaces documentation](https://huggingface.co/docs/hub/spaces-overview).
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/deploying_ploomber.md
```md
# Deploy to Ploomber Cloud
For production deployments, you can use Ploomber Cloud. It allows you to deploy
marimo in a secure and scalable way. See
[deployment instructions here](https://docs.cloud.ploomber.io/en/latest/apps/marimo.html)
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/index.md
```md
# Deploying
You can deploy marimo in three ways:
1. via an **edit server**, which allows you to create and edit notebooks. On
the CLI, this is launched with `marimo edit`, and is similar to `jupyter notebook`.
2. via a **run server**, which allows you serve marimo notebooks as read-only
web apps. On the CLI, this is launched with `marimo run notebook.py`
3. programmatically, which allows you serve **read-only** marimo apps
as part of other ASGI applications, for example using FastAPI.
!!! tip "Sharing lightweight notebooks on the web"
To share notebooks on the public web, try using [our online playground](https://marimo.new). Our playground runs entirely in the browser -- no
backend required, via [WASM](../../guides/wasm.md).
Or, to share notebooks with email-based authorization, you can also
try our free [community cloud](https://marimo.io/sign-up), which is
also powered by WASM.
WASM notebooks support most but not all Python features and packages.
## Deploying an edit server
Here are a few ways to deploy an edit server on a remote instance:
1. With [ssh-port forwarding](../../faq.md#faq-remote), using `marimo edit --headless`.
2. Via docker and our [prebuilt containers](prebuilt_containers.md).
3. Via a deployment service [such as Railway](deploying_railway.md).
4. [Behind JupyterHub](../../faq.md#faq-jupyter-hub).
## Deploying as read-only apps
These guides help you deploy marimo notebooks as read-only apps.
| | |
| :------------------------------ | :------------------------------------------------------- |
| [programmatically](./programmatically.md) | Programmatically run and customize read-only marimo apps |
| [deploying_docker](./deploying_docker.md) | Deploy with Docker |
| [authentication](./authentication.md) | Authentication and security |
| [deploying_public_gallery](./deploying_public_gallery.md) | Deploy to our public gallery |
| [deploying_hugging_face](./deploying_hugging_face.md) | Deploy to Hugging Face |
| [deploying_ploomber](./deploying_ploomber.md) | Deploy to Ploomber Cloud |
### Health and status endpoints
The following endpoints may be useful when deploying your application:
- `/health` - A health check endpoint that returns a 200 status code if the application is running as expected
- `/healthz` - Same as above, just a different name for easier integration with cloud providers
- `/api/status` - A status endpoint that returns a JSON object with the status of the server
### Configuration
If you would like to deploy your application at a subpath, you can set the `--base-url` flag when running your application.
```bash
marimo run app.py --base-url /subpath
```
### Including code in your application
You can include code in your application by using the `--include-code` flag when running your application.
```bash
marimo run app.py --include-code
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/prebuilt_containers.md
```md
# Prebuilt containers
marimo provides prebuilt containers for running a marimo server.
You can find the containers and tags on [marimo's GitHub packages page](https://github.com/marimo-team/marimo/pkgs/container/marimo).
We provide the following variants:
- `marimo:latest` - The latest version of marimo
- `marimo:latest-data` - The latest version of marimo with `altair`, `pandas`, and `numpy` preinstalled.
- `marimo:latest-sql` - The latest version of marimo with `marimo[sql]` and `duckdb` preinstalled.
or any particular version of marimo; for example, `marimo:0.8.3`, `marimo:0.8.3-data`, `marimo:0.8.3-sql`.
Each container is built on `3.12-slim`, but if you'd like to see different configurations, please file an issue or submit a PR!
## Running locally
To run the container locally, you can use the following command:
```bash
docker run -p 8080:8080 -it ghcr.io/marimo-team/marimo:latest-sql
```
## Use in a Dockerfile
To use a prebuilt container in a Dockerfile, you can use the following command:
```dockerfile
FROM ghcr.io/marimo-team/marimo:latest-sql
# Install any additional dependencies here
CMD ["marimo", "edit", "--no-token", "-p", "8080", "--host", "0.0.0.0"]
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/index.md
```md
# Editor features
The **marimo editor** is the browser-based IDE in which you write marimo
notebooks. We've taken a batteries-included approach to designing the editor:
it comes _packed_ with features to make you productive when working
with code and data.
| Guide | Description |
|-------|-------------|
| [Overview](overview.md) | An overview of editor features and configuration |
| [Package Management](package_management.md) | Using package managers in marimo |
| [AI Completion](ai_completion.md) | Code with the help of a language model |
| [Hotkeys](hotkeys.md) | Our hotkeys |
Highlights include:
- a variables panel that lets you explore variable values and see where they are defined
- a data explorer that lets you inspect dataframes and tables at a glance
- smart module autoreloading that tells you which cells need to be rerun
- code completion
- GitHub Copilot
- language-model assisted coding
- vim keybindings
- live documentation preiews as you type
and much more.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/hotkeys.md
```md
# Hotkeys
If you'd like to override the default hotkeys, you can do so in the hotkeys menu (`Ctrl/Cmd-Shift-h`), or modifying your `marimo.toml`.
You can find a list of available hotkeys below:
| Hotkey |
| --------------------------- |
| `cell.aiCompletion` |
| `cell.cellActions` |
| `cell.complete` |
| `cell.createAbove` |
| `cell.createBelow` |
| `cell.delete` |
| `cell.findAndReplace` |
| `cell.focusDown` |
| `cell.focusUp` |
| `cell.fold` |
| `cell.foldAll` |
| `cell.format` |
| `cell.goToDefinition` |
| `cell.hideCode` |
| `cell.moveUp` |
| `cell.moveDown` |
| `cell.moveLeft` |
| `cell.moveRight` |
| `cell.redo` |
| `cell.run` |
| `cell.runAndNewAbove` |
| `cell.runAndNewBelow` |
| `cell.selectNextOccurrence` |
| `cell.sendToBottom` |
| `cell.sendToTop` |
| `cell.splitCell` |
| `cell.undo` |
| `cell.unfold` |
| `cell.unfoldAll` |
| `cell.viewAsMarkdown` |
| `completion.moveDown` |
| `completion.moveUp` |
| `global.commandPalette` |
| `global.focusBottom` |
| `global.focusTop` |
| `global.foldCode` |
| `global.formatAll` |
| `global.hideCode` |
| `global.interrupt` |
| `global.runStale` |
| `global.save` |
| `global.showHelp` |
| `global.toggleLanguage` |
| `global.toggleTerminal` |
| `global.toggleSidebar` |
| `global.unfoldCode` |
| `markdown.blockquote` |
| `markdown.bold` |
| `markdown.code` |
| `markdown.italic` |
| `markdown.link` |
| `markdown.orderedList` |
| `markdown.unorderedList` |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/ai_completion.md
```md
# AI completion
marimo comes with GitHub Copilot, a tool that helps you write code faster by
suggesting in-line code suggestions based on the context of your current code.
marimo also comes with the ability to use AI for refactoring a cell, finishing writing a cell, or writing a full cell from scratch.
This feature is currently experimental and is not enabled by default.
## GitHub Copilot
The marimo editor natively supports [GitHub Copilot](https://copilot.github.com/),
an AI pair programmer, similar to VS Code.
_Get started with Copilot_:
1. Install [Node.js](https://nodejs.org/en/download).
2. Enable Copilot via the settings menu in the marimo editor.
!!! note "Installation Requirement"
Copilot is not yet available in our conda distribution; please install
marimo using ``pip`` if you need Copilot.
## Codeium Copilot
1. Go to the Codeium website and sign up for an account: <https://codeium.com/>
2. Install the browser extension: <https://codeium.com/chrome_tutorial>
3. Open the settings for the Chrome extension and click on "Get Token"
4. Right-click on the extension window and select "Inspect" to open the developer tools for the extension. Then click on "Network"
5. Copy the token and paste it into the input area, and then press "Enter Token"
6. This action will log a new API request in the **Network** tab. Click on "Preview" to get the API key.
7. Paste the API key in the marimo settings in the UI, or add it to your `marimo.toml` file as follows:
```toml title="marimo.toml"
[completion]
copilot = "codeium"
codeium_api_key = ""
```
### Alternative: Obtain Codeium API key using VS Code
1. Go to the Codeium website and sign up for an account: <https://codeium.com/>
2. Install the [Codeium Visual Studio Code extension](vscode:extension/codeium.codeium) (see [here](https://codeium.com/vscode_tutorial) for complete guide)
3. Sign in to your Codeium account in the VS Code extension
4. Select the Codeium icon on the Activity bar (left side), which opens the Codeium pane
5. Select the **Settings** button (gear icon) in the top-right corner of the Codeium pane
<div align="center">
<figure>
<img src="/_static/docs-ai-completion-codeium-vscode.png"/>
<figcaption>Open Codeium settings</figcaption>
</figure>
</div>
6. Click the **Download** link under the **Extension Diagnostics** section
7. Open the diagnostic file and search for `apiKey`
<div align="center">
<figure>
<img src="/_static/docs-ai-completion-codeium-vscode-download-diagnostics.png"/>
<figcaption>Download diagnostics file with API key</figcaption>
</figure>
</div>
8. Copy the value of the `apiKey` to `.marimo.toml` in your home directory
```toml title="marimo.toml"
[completion]
codeium_api_key = "a1e8..." # <-- paste your API key here
copilot = "codeium"
activate_on_typing = true
```
## Generate code with our AI assistant
marimo has built-in support for generating and refactoring code with AI, with a variety of providers. marimo works with hosted AI providers, such as OpenAI, Anthropic, and Google, as well as local models served via Ollama.
### Custom AI Rules
You can customize how the AI assistant behaves by adding rules in the marimo settings. These rules help ensure consistent code generation across all AI providers. You can find more information about marimo's supported plotting libraries and data handling in the [plotting guide](../working_with_data/plotting.md#plotting) and [working with data guide](../working_with_data/index.md).
<div align="center">
<figure>
<img src="/_static/docs-ai-completion-custom-assist-rules.png"/>
<figcaption>Configure custom AI rules in settings</figcaption>
</figure>
</div>
For example, you can add rules about:
- Preferred plotting libraries (matplotlib, plotly, altair)
- Data handling practices
- Code style conventions
- Error handling preferences
Example custom rules:
```
Use plotly for interactive visualizations and matplotlib for static plots
Prefer polars over pandas for data manipulation due to better performance
Include docstrings for all functions using NumPy style
Use Type hints for all function parameters and return values
Handle errors with try/except blocks and provide informative error messages
Follow PEP 8 style guidelines
When working with data:
- Use altair, plotly for declarative visualizations
- Prefer polars over pandas
- Ensure proper error handling for data operations
For plotting:
- Use px.scatter for scatter plots
- Use px.line for time series
- Include proper axis labels and titles
- Set appropriate color schemes
```
To locate your configuration file, run:
```bash
marimo config show
```
At the top, the path to your `marimo.toml` file will be shown. You can Ctrl/Cmd+click the path to open it in your editor. For more information about configuration, see the [Configuration Guide](../configuration/index.md).
Below we describe how to connect marimo to your AI provider. Once enabled, you can generate entirely new cells by clicking the "Generate with AI" button at the bottom of your notebook. You can also refactor existing cells by inputting `Ctrl/Cmd-Shift-e` in a cell, opening an input to modify the cell using AI.
<div align="center">
<figure>
<video src="/_static/ai-completion.mp4" controls="controls" width="100%" height="100%"></video>
<figcaption>Use AI to modify a cell by pressing `Ctrl/Cmd-Shift-e`.</figcaption>
</figure>
</div>
### Using OpenAI
1. Install openai: `pip install openai`
2. Add the following to your `marimo.toml`:
```toml title="marimo.toml"
[ai.open_ai]
# Get your API key from https://platform.openai.com/account/api-keys
api_key = "sk-proj-..."
# Choose a model, we recommend "gpt-4-turbo"
model = "gpt-4-turbo"
# Available models: gpt-4-turbo-preview, gpt-4, gpt-3.5-turbo
# See https://platform.openai.com/docs/models for all available models
# Change the base_url if you are using a different OpenAI-compatible API
base_url = "https://api.openai.com/v1"
```
### Using Anthropic
To use Anthropic with marimo:
1. Sign up for an account at [Anthropic](https://console.anthropic.com/) and grab your [Anthropic Key](https://console.anthropic.com/settings/keys).
2. Add the following to your `marimo.toml`:
```toml title="marimo.toml"
[ai.open_ai]
model = "claude-3-5-sonnet-20240620"
# or any model from https://docs.anthropic.com/en/docs/about-claude/models
[ai.anthropic]
api_key = "sk-ant-..."
```
### Using Google AI
To use Google AI with marimo:
1. Sign up for an account at [Google AI Studio](https://aistudio.google.com/app/apikey) and obtain your API key.
2. Install the Google AI Python client: `pip install google-generativeai`
3. Add the following to your `marimo.toml`:
```toml title="marimo.toml"
[ai.open_ai]
model = "gemini-1.5-flash"
# or any model from https://ai.google.dev/gemini-api/docs/models/gemini
[ai.google]
api_key = "AI..."
```
### Using local models with Ollama { #using-ollama }
Ollama allows you to run open-source LLMs on your local machine. To integrate Ollama with marimo:
1. Download and install [Ollama](https://ollama.com/).
2. Download the model you want to use:
```bash
# View available models at https://ollama.com/library
ollama pull llama3.1
ollama pull codellama # recommended for code generation
# View your installed models
ollama ls
```
3. Start the Ollama server in a terminal:
```bash
ollama serve
# In a new terminal
ollama run codellama # or any model from ollama ls
```
4. Visit <http://127.0.0.1:11434> to confirm that the server is running.
!!! note "Port already in use"
If you get a "port already in use" error, you may need to close an existing Ollama instance. On Windows, click the up arrow in the taskbar, find the Ollama icon, and select "Quit". This is a known issue (see [Ollama Issue #3575](https://github.com/ollama/ollama/issues/3575)). Once you've closed the existing Ollama instance, you should be able to run `ollama serve` successfully.
5. Open a new terminal and start marimo:
```bash
marimo edit notebook.py
```
6. Add the following to your `marimo.toml`:
```toml title="marimo.toml"
[ai.open_ai]
api_key = "ollama" # This is not used, but required
model = "codellama" # or another model from `ollama ls`
base_url = "http://127.0.0.1:11434/v1"
```
### Using other AI providers
marimo supports OpenAI's API by default. Many providers offer OpenAI API-compatible endpoints, which can be used by simply changing the `base_url` in your configuration. For example, providers like [GROQ](https://console.groq.com/docs/openai) and [DeepSeek](https://platform.deepseek.com) follow this pattern.
??? tip "Using OpenAI-compatible providers (e.g., DeepSeek)"
=== "Via marimo.toml"
Add the following configuration to your `marimo.toml` file:
```toml
[ai.open_ai]
api_key = "dsk-..." # Your provider's API key
model = "deepseek-chat" # or "deepseek-reasoner"
base_url = "https://api.deepseek.com/"
```
=== "Via UI Settings"
1. Open marimo's Settings panel
2. Navigate to the AI section
3. Enter your provider's API key in the "OpenAI API Key" field
4. Under AI Assist settings:
- Set Base URL to your provider's endpoint (e.g., `https://api.deepseek.com`)
- Set Model to your chosen model (e.g., `deepseek-chat` or `deepseek-reasoner`)
For a comprehensive list of compatible providers and their configurations, please refer to the [liteLLM Providers documentation](https://litellm.vercel.app/docs/providers).
For providers not compatible with OpenAI's API, please submit a [feature request](https://github.com/marimo-team/marimo/issues/new?template=feature_request.yaml) or "thumbs up" an existing one.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/deploying/programmatically.md
```md
# Running the marimo backend programmatically
marimo can be run programmatically using the `marimo` module. This is useful when you want to run marimo as part of a larger application or when you need to customize the behavior of marimo (e.g. middleware, custom error handling, authentication, routing, etc).
## FastAPI Example
Here's an example of how you can run a marimo application programmatically using FastAPI:
```python
from typing import Annotated, Callable, Coroutine
from fastapi.responses import HTMLResponse, RedirectResponse
import marimo
from fastapi import FastAPI, Form, Request, Response
# Create a marimo asgi app
server = (
marimo.create_asgi_app()
.with_app(path="", root="./pages/index.py")
.with_app(path="/dashboard", root="./pages/dashboard.py")
.with_app(path="/sales", root="./pages/sales.py")
)
# Create a FastAPI app
app = FastAPI()
app.add_middleware(auth_middleware)
app.add_route("/login", my_login_route, methods=["POST"])
app.mount("/", server.build())
# Run the server
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="localhost", port=8000)
```
For a more complete example, see the [FastAPI example](https://github.com/marimo-team/marimo/tree/main/examples/frameworks/fastapi).
## Dynamic directory
If you'd like to create a server to dynamically load marimo notebooks from a directory, you can use the `with_dynamic_directory` method. This is useful if the contents of the directory change often, such as a directory of notebooks for a dashboard, without restarting the server.
```python
server = (
marimo.create_asgi_app()
.with_dynamic_directory(path="/dashboard", directory="./notebooks")
)
```
If the notebooks in the directory are expected to be static, it is better to use the `with_app` method and loop through the directory contents.
```python
from pathlib import Path
server = marimo.create_asgi_app()
app_names: list[str] = []
notebooks_dir = Path(__file__).parent / "notebooks"
for filename in sorted(notebooks_dir.iterdir()):
if filename.suffix == ".py":
app_name = filename.stem
server = server.with_app(path=f"/{app_name}", root=filename)
app_names.append(app_name)
```
## Accessing Request Data
Inside your marimo notebooks, you can access the current request data using `mo.app_meta().request`. This is particularly useful when implementing authentication or accessing user data.
```python
import marimo as mo
# Access request data in your notebook
request = mo.app_meta().request
if request and request.user and request.user["is_authenticated"]:
content = f"Welcome {request.user['username']}!"
else:
content = "Please log in"
mo.md(content)
```
### Authentication Middleware Example
Here's an example of how to implement authentication middleware that populates `request.user`:
```python
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.requests import Request
class AuthMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next):
# Add user data to the request scope
# This will be accessible via mo.app_meta().request.user
request.scope["user"] = {
"is_authenticated": True,
"username": "example_user",
# Add any other user data
}
response = await call_next(request)
return response
# Add the middleware to your FastAPI app
app.add_middleware(AuthMiddleware)
```
The `request` object provides access to:
- `request.headers`: Request headers
- `request.cookies`: Request cookies
- `request.query_params`: Query parameters
- `request.path_params`: Path parameters
- `request.user`: User data added by authentication middleware
- `request.url`: URL information including path, query parameters
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/overview.md
```md
# Editor overview
This guide introduces some of marimo editor's features, including
a variables panel, dependency graph viewer, table of contents, HTML export,
GitHub copilot, code formatting, a feedback form, and more.
## Configuration
The editor exposes of a number of settings for the current notebook,
as well as user-wide configuration that will apply to all your notebooks.
These settings include the option to display the current notebook in
full width, to use vim keybindings, to enable GitHub copilot, and more.
To access these settings, click the gear icon in the top-right of the editor:
<div align="center">
<img src="/_static/docs-user-config.png" />
</div>
A non-exhaustive list of settings:
- Outputs above or below code cells
- [Disable/enable autorun](../reactivity.md#configuring-how-marimo-runs-cells)
- Package installation
- Vim keybindings
- Dark mode
- Auto-save
- Auto-complete
- Editor font-size
- Code formatting with ruff/black
- [GitHub Copilot](ai_completion.md)
- [LLM coding assistant](ai_completion.md)
- [Module autoreloading](../configuration/runtime_configuration.md#on-module-change)
### Vim keybindings
marimo supports vim keybindings.
**Additional bindings/features:**
- `gd` - go to definition
- `dd` - when a cell is empty, delete it
## Overview panels
marimo ships with the IDE panels that provide an overview of your notebook
- **file explorer**: view the file tree, open other notebooks
- **variables**: explore variable values, see where they are defined and used, with go-to-definition
- **data explorer**: see dataframe and table schemas at a glance
- **dependency graph**: view dependencies between cells, drill-down on nodes and edges
- **package manager**: add and remove packages, and view your current environment
- **table of contents**: corresponding to your markdown
- **documentation** - move your text cursor over a symbol to see its documentation
- **logs**: a continuous stream of stdout and stderr
- **scratchpad**: a scratchpad cell where you can execute throwaway code
- **snippets** - searchable snippets to copy directly into your notebook
- **feedback** - share feedback!
These panels can be toggled via the buttons in the left of the editor.
## Cell actions
Click the three dots in the top right of a cell to pull up a context menu,
letting you format code, hide code, send a cell to the top or bottom of the
notebook, give the cell a name, and more.
Drag a cell using the vertical dots to the right of the cell.
## Right-click menus
marimo supports context-sensitive right-click menus in various locations of
the editor. Right-click on a cell to open a context-sensitive menu; right click
on the create-cell button (the plus icon) to get options for the cell type to
create.
## Go-to-definition
- Click on a variable in the editor to see where it's defined and used
- `Cmd/Ctrl-Click` on a variable to jump to its definition
- Right-click on a variable to see a context menu with options to jump to its definition
## Keyboard shortcuts
We've kept some well-known [keyboard
shortcuts](hotkeys.md) for notebooks (`Ctrl-Enter`, `Shift-Enter`), dropped others, and added a few of our own. Hit `Ctrl/Cmd-Shift-H` to pull up the shortcuts.
We know keyboard shortcuts are very personal; you can remap them in the
configuration.
_Missing a shortcut? File a
[GitHub issue](https://github.com/marimo-team/marimo/issues)._
## Command palette
Hit `Cmd/Ctrl+K` to open the command palette.
<div align="center">
<figure>
<img src="/_static/docs-command-palette.png"/>
<figcaption>Quickly access common commands with the command palette.</figcaption>
</figure>
</div>
_Missing a command? File a
[GitHub issue](https://github.com/marimo-team/marimo/issues)._
## Editor widths
You can set the width of the editor in the notebook settings:
- **Compact**: A narrow width with generous margins, ideal for reading
- **Wide**: A wider layout that gives more space for content
- **Full**: Uses the full width of your browser window, ideal for dashboard-style notebooks
- **Multi-column**: Splits your notebook into multiple columns, letting you view and edit cells side-by-side. This is only possible because marimo models your notebook as a directed acyclic graph (DAG) and the [execution order](../reactivity.md#execution-order) is determined by the relationships between
cells and their variables, not by the order of cells on the page.
<div align="center">
<figure>
<img src="/_static/docs-multi-column.png"/>
<figcaption>Multi-column notebook</figcaption>
</figure>
</div>
## Share on our online playground
Get a link to share your notebook via our [online playground](../wasm.md):
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/share-wasm-link.mp4" type="video/mp4">
<source src="/_static/share-wasm-link.webm" type="video/webm">
</video>
</figure>
</div>
_Our online playground uses WebAssembly. Most but not all packages on PyPI
are supported. Local files are not synchronized to our playground._
## Export to static HTML
Export the current view your notebook to static HTML via the notebook
menu:
<div align="center">
<figure>
<img src="/_static/docs-html-export.png"/>
<figcaption>Download as static HTML.</figcaption>
</figure>
</div>
You can also export to HTML at the command-line:
```bash
marimo export html notebook.py -o notebook.html
```
## Send feedback
The question mark icon in the panel tray opens a
dialog to send anonymous feedback. We welcome any and all feedback, from the
tiniest quibbles to the biggest blue-sky dreams.
<div align="center">
<figure>
<img src="/_static/docs-feedback-form.png"/>
<figcaption>Send anonymous feedback with our feedback form.</figcaption>
</figure>
</div>
If you'd like your feedback to start a conversation (we'd love to talk with
you!), please consider posting in our [GitHub
issues](https://github.com/marimo-team/marimo/issues) or
[Discord](https://marimo.io/discord?ref=docs). But if you're in a flow state and
can't context switch out, the feedback form has your back.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/module_autoreloading.md
```md
# Module autoreloading
marimo has an advanced module autoreloader built-in, which you can
enable in the [notebook settings](../configuration/runtime_configuration.md).
When you make edits to Python modules that your notebook has imported, the
module autoreloader will automatically mark cells that use them as stale and,
optionally, automatically run them.
!!! question "Why autoreload?"
Autoreloading enables a workflow that many developers find
productive: develop complex logic in Python modules, and use the marimo
notebook as a DAG or main script that orchestrates your logic.
Based on static analysis, the reloader only runs cells affected by your edits.
The reloader is recursive, meaning that marimo tracks modifications for modules
imported by your notebook's imported modules too. These two featuers make
marimo's module autoreloader far more advanced than IPython's.
Autoreloading comes in two types:
1. **autorun**: automatically re-runs cells affected by module modification.
<figure>
<video controls loop width="100%" height="100%" align="center" src="/_static/docs-module-reloading.mp4"> </video>
<figcaption align="center">When set to autorun, marimo's reloader automatically run cells when you edit Python files.</figcaption>
</figure>
2. **lazy**: marks cells affected by module modifications as stale, letting you know which cells need to be re-run.
<figure>
<video controls loop width="100%" height="100%" align="center" src="/_static/docs-module-reloading-lazy.mp4"> </video>
<figcaption align="center">When set to lazy, marimo's reloader marks cells as stale when you edit Python files.</figcaption>
</figure>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/watching.md
```md
# Using your own editor
While we recommend using the [marimo editor](index.md),
we understand that you may prefer to use your own. marimo provides a
`--watch` flag that watches your notebook file for changes, syncing them to
the marimo editor or running application. This lets you edit your notebook
using an editor of your choice, like neovim, VSCode, Cursor, or PyCharm, and
have the changes automatically reflected in your browser.
!!! tip "Install watchdog for better file watching"
For better performance, install [watchdog](https://pypi.org/project/watchdog/).
Without watchdog, marimo resorts to polling.
## `marimo edit --watch`
When you run `marimo edit` with the `--watch` flag, the marimo server
will open your notebook in the browser and watch the underlying notebook
file for changes. When you make changes to the notebook file, they will be
streamed to the marimo editor in the browser.
Synced code will not be executed automatically, with cells marked as stale instead.
Run all stale cells with the marimo editor's "Run" button, or the [`runStale`
hotkey](hotkeys.md), to see the new outputs.
!!! note "Cell signature and returns"
Don't worry about maintaining the signatures of cells and their return
values; marimo will handle this for you.
## `marimo run --watch`
When you run a notebook with the `--watch` flag, whenever the file watcher
detects a change to the notebook file, the application will be refreshed. The
browser will trigger a page refresh to ensure your notebook starts from a fresh
state.
## Watching for changes to other modules
marimo can also watch for changes to Python modules that your notebook imports,
letting you edit auxiliary Python files in your own editor as well. Learn how
to enable this feature in our [Module Autoreloading
Guide](module_autoreloading.md)
## Watching for data changes
!!! note
Support for watching data files and automatically refreshing cells that depend on them is coming soon. Follow along at <https://github.com/marimo-team/marimo/issues/3258>
## Hot-reloading WebAssembly notebooks
Follow these steps to develop a notebook using your own editor while
previewing it as a [WebAssembly notebook](../wasm.md) in the browser. This lets
you take advantage of local development tools while seeing the notebook as it
appears when deployed as a WebAssembly notebook.
```bash
# in one terminal, start a watched edit (or run) session
marimo edit notebook.py --watch
# in another terminal
marimo export html-wasm notebook.py -o output_dir --watch
# in a third terminal, serve the WASM application
cd path/to/output_dir
python -m http.server # or a server that watches for changes
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/integrating_with_marimo/displaying_objects.md
```md
# Richly display objects
marimo has built-in rich representations of many objects, including native
Python objects like lists and dicts as well as marimo objects like [UI
elements](../interactivity.md) and libraries, including matplotlib,
seaborn, Plotly, altair pandas, and more. These rich representations are
displayed for the last expression of a cell, or when using
[`mo.output.append`][marimo.output.append].
You can register rich displays with marimo for your own objects. You have
three options:
1. Implement a `_display_()` method
2. Implement a `_mime_()` method
3. Implement an IPython-style `_repr_*_()` method
If you can't modify the object, you can also add a formatter to the marimo library (option 4).
The return value of these methods determines what is shown. `_display_`
has the highest precedence, then built-in formatters, then `_mime_`, then `IPython` style `_repr_*_`
methods.
## Option 1: Implement a `_display_()` method
If an object implements a `_display_()`, marimo will use its return value
to visualize the object as an output.
For example:
```python
class Dice:
def _display_(self):
import random
return f"You rolled {random.randint(0, 7)}"
```
The return value of `_display_` can be any Python object, for example a
a matplotlib plot, a dataframe, a list, `mo.Html`, or a `mo.ui` element, and
marimo will attempt to display it.
In addition to being the most convenient way do define a custom display in
marimo (in terms of syntax), it is also helpful for library developers: this
option lets you make an object showable in marimo without adding marimo as a
dependency to your project.
However, if you need to display an object that marimo does not know how to
render (for example, maybe you are building a new plotting library), then
you need to consider of the other options below.
## Option 2: Implement an IPython `_repr_*_()` method
marimo can render objects that implement
[IPython's `_repr_*_()` protocol](https://ipython.readthedocs.io/en/stable/config/integrating.html#custom-methods)
for rich display. Here is an example of implementing `_repr_html_`, borrowed
from IPython's documentation:
```python
class Shout:
def __init__(self, text):
self.text = text
def _repr_html_(self):
return "<h1>" + self.text + "</h1>"
```
We support the following methods:
- `_repr_html_`
- `_repr_mimebundle_`
- `_repr_svg_`
- `_repr_json_`
- `_repr_png_`
- `_repr_jpeg_`
- `_repr_markdown_`
- `_repr_latex_`
- `_repr_text_`
## Option 3: Implement a `_mime_` method
When displaying an object, marimo's media viewer checks for the presence of a
method called `_mime_`. This method should take no arguments and return
a tuple of two strings, the [mime type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) and data to be displayed as a string.
**Examples.**
/// marimo-embed
size: medium
mode: edit
```python
@app.cell(hide_code=True)
def __():
mo.md("**JSON**")
@app.cell
def __():
import json
class MyJSONObject(object):
def __init__(self, data: dict[str, object]) -> None:
self.data = data
def _mime_(self) -> tuple[str, str]:
return ("application/json", json.dumps(self.data))
MyJSONObject({"hello": "world"})
@app.cell(hide_code=True)
def __():
mo.md("**HTML**")
@app.cell
def __():
class Colorize(object):
def __init__(self, text: str) -> None:
self.text = text
def _mime_(self) -> tuple[str, str]:
return (
"text/html",
"<span style='color:red'>" + self.text + "</span>",
)
Colorize("Hello!")
@app.cell(hide_code=True)
def __():
mo.md("**Image**")
@app.cell
def __():
class Image(object):
def __init__(self, url: str) -> None:
self.url = url
def _mime_(self) -> tuple[str, str]:
return ("image/png", self.url)
Image("https://raw.githubusercontent.com/marimo-team/marimo/main/docs/_static/marimo-logotype-thick.svg")
```
///
## Option 4: Add a formatter to the marimo repo
The recommended way to render rich displays of objects in marimo is to
implement `_display_` if possible, otherwise either the IPython `_repr_*_()_`
protocol or marimo's `_mime_()` protocol. If you are a a user of a library that
does not render properly in marimo, consider asking the library maintainers to
implement one of these protocols.
If it is not possible to implement a renderer protocol on the type
you want displayed, we will consider contributions to add formatters to the
marimo codebase. [Look at our codebase for
examples](https://github.com/marimo-team/marimo/tree/main/marimo/_output/formatters),
then open a pull request.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/integrating_with_marimo/index.md
```md
# Integrating with marimo
These guides will help you integrate your objects with marimo and hook
into marimo's reactive execution engine for UI plugins.
Still need help? Reach out to us on [Discord](https://marimo.io/discord?ref=docs) or
[GitHub issues](https://github.com/marimo-team/marimo/issues).
!!! tip "Checking if running in a marimo notebook"
You can check if Python is running in a marimo notebook with
[`mo.running_in_notebook`][marimo.running_in_notebook]. This can be helpful
when developing library code that integrates with marimo.
| Guide | Description |
|-------|-------------|
| [Displaying Objects](displaying_objects.md) | Richly display objects by hooking into marimo's media viewer |
| [Custom UI Plugins](custom_ui_plugins.md) | Build custom UI plugins that hook into marimo's reactive execution engine |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/editor_features/package_management.md
```md
# Package management
marimo supports package management for `pip, rye, uv, poetry, pixi`. When marimo comes across a module that is not installed, you will be prompted to install it using your preferred package manager.
Once the module is installed, all cells that depend on the module will be rerun.
!!! note "Package Installation"
We use some heuristic for guessing the package name in your registry (e.g. PyPI) from the module name. It is possible that the package name is different from the module name. If you encounter an error, please file an issue or help us by adding your mapping [directly to the codebase](https://github.com/marimo-team/marimo/blob/main/marimo/_runtime/packages/module_name_to_pypi_name.py).
## Package reproducibility
marimo is the only Python notebook that is reproducible down to the packages
they use. This makes it possible to share standalone notebooks without shipping
`requirements.txt` files alongside them, and guarantees your notebooks will
work weeks, months, even years into the future.
To learn more, see the [Package Reproducibility Guide](../package_reproducibility.md).
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/community_cloud/index.md
```md
# Community Cloud
Our [Community Cloud](https://marimo.io/dashboard) is a free workspace
for creating, saving, and sharing marimo notebooks. Unlike the
[Playground](../playground.md), the Community Cloud requires a login. In
return, it lets you save noteoboks, share them using email-based authorization,
and upload a limited amount of data.
!!! note "WebAssembly notebooks only"
Currently, the Community Cloud only allows the creation of [WebAssembly
notebooks](../../wasm.md). These are easy to share and embed in other
web pages, but have some limitations in packages and performance.
Note: unlike our other publishing options, it is not possible to embed
editable Community Cloud notebooks in other web pages.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/integrating_with_marimo/custom_ui_plugins.md
```md
Build custom UI plugins that hook into marimo’s reactive execution engine by
using [anywidget](https://anywidget.dev/). [See our AnyWidget API
docs](../../api/inputs/anywidget.md) for more information.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/deploy.md
```md
# Deploy on backends
If you cannot use WebAssembly notebooks, you can deploy marimo notebooks via a
traditional client-server model.
Both the edit server can be deployed as well as individual notebooks (as
readonly apps).
Learn more in our [Deployment Guide](../deploying/index.md).
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/index.md
```md
# Publishing notebooks to the web
You can publish marimo notebooks to the web as interactive editable notebooks,
readonly web apps, or [static documents](../exporting.md).
Thanks to [WebAssembly](../wasm.md), you can even share executable
notebooks on GitHub Pages or other static sites without paying for backend
infrastrcture. This makes it easy to share your work with colleagues, embed
executable notebooks in web documentation or educational websites, and more.
This guide provides an overview of the various ways to publish marimo notebooks.
| Guide | Description |
| ----------------------------------------------------- | ------------------------------------------------------------ |
| [Embedding](embedding.md) | An overview of embedding notebooks in other sites |
| [From GitHub](from_github.md) | How to share links to executable notebooks hosted on GitHUb |
| [GitHub Pages](github_pages.md) | Publishing interactive notebooks on GitHub Pages |
| [Online playground](playground.md) | Sharing notebook links using our online playground |
| [Community Cloud](community_cloud/index.md) | Save notebooks to our free Community Cloud |
| [Self-host WebAssembly notebooks](self_host_wasm.md) | Self-hosting interactive WebAssembly (HTML export) notebooks |
| [View notebooks on GitHub](view_outputs_on_github.md) | Viewing notebook outputs on GitHub |
| [Deploy on a backend](deploy.md) | Deploying notebooks on backends |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/from_github.md
```md
# From GitHub
marimo makes it very easy to share links to executable notebooks from notebooks
hosted on GitHub. Unlike Google Colab, marimo also automatically synchronizes
data stored in your GitHub repo to the notebook's filesystem, making it
easy to bundle data with your notebooks.
- Publish notebooks to [GitHub Pages](github_pages.md)
- Edit notebooks on the [marimo playground](playground.md), with public link-based sharing
(no login required!)
- Synchronize to our [Community Cloud](community_cloud/index.md) for email-based sharing
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/embedding.md
```md
# Embedding
There are various ways to embed marimo notebooks in other web pages, such
as web documentation, educational platforms, or static sites in general. Here
are two ways:
* Host on [GitHub Pages](github_pages.md) or [self-host WASM HTML](self_host_wasm.md),
and `<iframe>` the published notebook.
* `<iframe>` a [playground](playground.md) notebook, and [customize the embedding](playground.md#embedding-in-other-web-pages) with query params.
(This is what we do throughout docs.marimo.io.)
We plan to provide more turn-key solutions for static site generation with
marimo notebooks in the future.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/playground.md
```md
# Online playground
Our [online playground](https://marimo.app) lets you
create and share marimo notebooks for free, without creating an account.
Playground notebooks are great for embedding in other web pages — all the
embedded notebooks in marimo's own docs are playground notebooks. They
are also great for sharing via links.
**Try our playground!** Just navigate to
[https://marimo.new](https://marimo.new).
!!! note "WebAssembly notebooks only"
Currently, the online playground only allows the creation of [WebAssembly
notebooks](../wasm.md). These are easy to share and embed in other
web pages, but have some limitations in packages and performance.
_The notebook embedded below is a playground notebook!_
<iframe src="https://marimo.app/l/upciwv?embed=true" width="100%" height=400 frameBorder="0"></iframe>
## Creating and sharing playground notebooks { #creating-and-sharing-playground-notebooks }
Playground notebooks run at [marimo.app](https://marimo.app).
### New notebooks
To create a new playground notebook, visit <https://marimo.new>.
Think of [marimo.new](https://marimo.new) as a
scratchpad for experimenting with code, data, and models and for prototyping
tools, available to you at all times and on all devices.
!!! tip "Saving playground notebooks"
When you save a WASM notebook, a copy of your code is saved to your
web browser's local storage. When you return to
[marimo.app](https://marimo.app), the last notebook you worked on will be
re-opened. You can also click a button to save your notebook to
the [Community Cloud](community_cloud/index.md).
### Share via links
At [marimo.app](https://marimo.app), save your notebook and then click the
`Create permalink` button to generate a shareable permalink to your
notebook.
Please be aware that marimo permalinks are publicly accessible.
### Open notebooks hosted on GitHub
To open notebooks hosted on GitHub in the playground, just
navigate to `https://marimo.app/path/to/notebook.py`. For example:
<https://marimo.app/github.com/marimo-team/marimo/blob/main/examples/ui/slider.py>.
!!! tip "Use our bookmarklet!"
For a convenient way to create notebooks from GitHub, drag and drop the
following button to your bookmarks bar:
<a href="javascript:(function(){if(!location.href.endsWith('.py')&&!location.href.endsWith('.ipynb')){alert('Please use this bookmarklet on a URL ending with .py or .ipynb');return;}let url=window.location.href.replace(/^https:\/\//,'');window.open('https://marimo.app/' + url, '_blank');})();"
style="padding: 5px 10px; background-color: #007bff; color: white; text-decoration: none; border-radius: 5px; font-weight: bold;">
Open in marimo
</a>
Clicking the bookmark when you are viewing a notebook will
open it in [marimo.app](https://marimo.app/).
!!! tip "From Jupyter notebooks"
You can also create Playground notebooks from Jupyter notebooks hosted
on GitHub. marimo will attempt to automatically convert the notebook
to a marimo notebook.
#### Including data files
Notebooks created from GitHub links have the entire contents of the repository
mounted into the notebook's filesystem. This lets you work with files
using regular Python file I/O!
When constructing paths to data files, make sure to use
[`mo.notebook_dir()`][marimo.notebook_dir] to ensure that paths work both
locally and in the playground.
!!! example "Example"
Navigate to
<https://marimo.app/github.com/marimo-team/marimo/blob/main/examples/misc/notebook_dir.py>
and open the file explorer panel to see all the files available to the notebook.
#### Open in marimo badge
Include an "open in marimo" badge in your README to link to playground
notebooks hosted on GitHub:
[![Open with marimo](https://marimo.io/shield.svg)](https://marimo.app/GITHUB_URL)
=== "Markdown"
Replace `GITHUB_URL` with the URL to a notebook on GitHub.
```markdown
[![Open with marimo](https://marimo.io/shield.svg)](https://marimo.app/GITHUB_URL)
```
=== "HTML"
Replace `GITHUB_URL` with the URL to a notebook on GitHub.
```html
<a href="https://marimo.app/GITHUB_URL" target="_blank">
<img alt="Open in marimo" src="https://marimo.io/shield.svg" />
</a>
```
### Creating playground notebooks from local notebooks
In the marimo editor's notebook action menu, use `Share > Create WebAssembly
link` to get a `marimo.app/...` URL representing your notebook:
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/share-wasm-link.mp4" type="video/mp4">
<source src="/_static/share-wasm-link.webm" type="video/webm">
</video>
</figure>
</div>
WASM notebooks come with common Python packages installed, but you may need to
[install additional packages using micropip](../wasm.md#supported-packages).
The obtained URL encodes your notebook code as a parameter, so it can be
quite long. If you want a URL that's easier to share, you can [create a
shareable permalink](#share-via-links).
## Configuration
Your `marimo.app` URLs can be configured using the following parameters.
### Read-only mode
To view a notebook in read-only mode, with
code cells locked, append `&mode=read` to your URL's list of query parameters
(or `?mode=read` if your URL doesn't have a query string).
Example:
- `https://marimo.app/l/83qamt?mode=read`
### Hide header for embedding
To hide the `marimo.app` header, append `&embed=true` to your URL's list of query
parameters (or `?embed=true` if your URL doesn't have a query string).
Example:
- `https://marimo.app/l/83qamt?embed=true`
- `https://marimo.app/l/83qamt?mode=read&embed=true`
See the [section on embedding](#embedding-in-other-web-pages) for examples of
how to embed marimo notebooks in your own webpages.
### Excluding code
By default, WASM notebooks expose your Python code to viewers. If you've
enabled read-only mode, you can exclude code with
`&include-code=false`. If you want to include code but have it be hidden
by default, use the parameter `&show-code=false`.
A sufficiently determined user would still be able
to obtain your code, so **don't** think of this as a security feature; instead,
think of it as an aesthetic or practical choice.
## Embedding in other web pages
WASM notebooks can be embedded into other webpages using the HTML `<iframe>`
tag.
### Embedding a blank notebook
Use the following snippet to embed a blank marimo notebook into your web page,
providing your users with an interactive code playground.
```html
<iframe
src="https://marimo.app/l/aojjhb?embed=true"
width="100%"
height="500"
frameborder="0"
></iframe>
```
<iframe src="https://marimo.app/l/aojjhb?embed=true" width="100%" height="500" frameBorder="0"></iframe>
### Embedding an existing notebook
To embed existing marimo notebooks into a webpage, first, [obtain a
URL to your notebook](#creating-and-sharing-playground-notebooks), then put it in an iframe.
```html
<iframe
src="https://marimo.app/l/c7h6pz?embed=true"
width="100%"
height="500"
frameborder="0"
></iframe>
```
<iframe src="https://marimo.app/l/c7h6pz?embed=true" width="100%" height="500" frameBorder="0"></iframe>
### Embedding an existing notebook in read-only mode
You can optionally render embedded notebooks in read-only mode by appending
`&mode=read` to your URL.
```html
<iframe
src="https://marimo.app/l/c7h6pz?mode=read&embed=true"
width="100%"
height="500"
frameborder="0"
></iframe>
```
<iframe src="https://marimo.app/l/c7h6pz?mode=read&embed=true" width="100%" height="500" frameBorder="0"></iframe>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/self_host_wasm.md
```md
# Self-host WebAssembly notebooks
As an alternative to [GitHub Pages](github_pages.md), it is possible to self-host
exported [WebAssembly notebooks](../wasm.md):
- [Export to WASM HTML](../exporting.md#export-to-wasm-powered-html).
- Serve the exported file over HTTP.
- Serve the assets in the `assets` directory, next to the HTML file.
- Possibly configure your web server to support serving `application/wasm/`
files with the correct headers.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/github_pages.md
```md
# Publish to GitHub Pages
You can publish executable notebooks to [GitHub Pages](https://pages.github.com/)
for free, after exporting your notebook to a WebAssembly notebook.
## Export to WASM-powered HTML
Export your notebook to a self-contained HTML file that runs using [WebAssembly](../wasm.md):
/// tab | Export as a readonly app
```bash
marimo export html-wasm notebook.py -o output_dir --mode run
```
///
/// tab | Export as an editable notebook
```bash
marimo export html-wasm notebook.py -o output_dir --mode edit
```
///
See our [exporting guide](../exporting.md#export-to-wasm-powered-html) for
the full documentation.
## Publish using GitHub Actions
/// tip | Template repository
Fork our [template repository](https://github.com/marimo-team/marimo-gh-pages-template) for deploying multiple notebooks to GitHub Pages. Once you have forked the repository, add your notebooks to the `notebooks` or `apps` directories,
for editable or readonly respectively.
///
Publish to GitHub Pages using the following GitHub Actions workflow,
which will republish your notebook on git push.
```yaml
jobs:
build:
runs-on: ubuntu-latest
steps:
# ... checkout and install dependencies
- name: 📄 Export notebook
run: |
marimo export html-wasm notebook.py -o path/to/output --mode run
- name: 📦 Upload Pages Artifact
uses: actions/upload-pages-artifact@v3
with:
path: path/to/output
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
permissions:
pages: write
id-token: write
steps:
- name: 🌐 Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
with:
artifact_name: github-pages
```
## Publish manually
You can also publish an exported notebook manually through your repository
settings. Read [GitHub's documentation](https://docs.github.com/en/pages/getting-started-with-github-pages/configuring-a-publishing-source-for-your-github-pages-site) to learn more.
Make sure to [include a `.nojekyll`
file](https://github.blog/news-insights/bypassing-jekyll-on-github-pages/) in
root folder from which your site is built to prevent GitHub from interfering
with your site.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/testing/pytest.md
```md
# Running unit tests with pytest
Since marimo notebooks are Python programs, you can test them using
[`pytest`](https://docs.pytest.org/en/stable/), a popular testing framework
for Python.
For example,
```bash
pytest test_notebook.py
```
runs and tests all notebook cells whose names start with `test_`.
!!! tip "Naming cells"
Name a cell by giving its function a name in the notebook file, or using
the cell action menu in the notebook editor.
!!! note "Use marimo notebooks just like normal pytest tests"
Include test notebooks (notebooks whose names start with `test_`) in your
standard test suite, and `pytest` will discover them automatically.
In addition, you can write self-contained notebooks that contain their own
unit tests, and run `pytest` on them directly (`pytest my_notebook.py`).
## Example
Running `pytest` on
```python
# content of test_notebook.py
import marimo
__generated_with = "0.10.6"
app = marimo.App()
@app.cell
def _():
def inc(x):
return x + 1
return inc
@app.cell
def test_answer(inc):
assert inc(3) == 5, "This test fails"
@app.cell
def test_sanity(inc):
assert inc(3) == 4, "This test passes"
```
prints
```pytest
============================= test session starts ==============================
platform linux -- Python 3.11.10, pytest-8.3.3, pluggy-1.5.0
rootdir: /notebooks
configfile: pyproject.toml
collected 2 items
test_notebook.py F. [100%]
=================================== FAILURES ===================================
__________________________________ test_fails __________________________________
import marimo
__generated_with = "0.10.6"
app = marimo.App(width="medium")
@app.cell
def _():
def inc(x):
return x + 1
return (inc,)
@app.cell
def test_answser(inc):
> assert inc(3) == 5, "This test fails"
E AssertionError: This test fails
test_notebook.py:16: AssertionError
=========================== short test summary info ============================
FAILED test_notebook.py::test_fails - AssertionError: This test fails
========================= 1 failed, 1 passed in 0.20s ===========================
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/testing/doctest.md
```md
# Testing with doctest
You can test code snippets in docstrings using
[doctest](https://docs.python.org/3/library/doctest.html), a Python module for
testing code snippets in documentation. This works because marimo notebooks are
just Python programs.
See this notebook for an example:
/// marimo-embed-file
filepath: examples/testing/running_doctests.py
///
```
File: /Users/morganmcguire/ML/marimo/docs/guides/testing/index.md
```md
# Testing notebooks
Because marimo notebooks are stored as Python, test them like any other Python
program.
| Guide | Description |
| --------------------- | ----------------------------------------------------------------------- |
| [pytest](pytest.md) | Include unit tests in notebooks, or implement entire tests as notebooks |
| [doctest](doctest.md) | Test code snippets in docstrings using doctest |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/working_with_data/index.md
```md
# Working with data
These guides introduce you to marimo's features for working with data,
including SQL cells, no-code dataframe transformation tools, and plots whose
selections are automatically sent back to Python.
| Guide | Description |
|-------|-------------|
| [SQL](sql.md) | Use SQL to query dataframes, databases, CSVs, etc. |
| [Dataframes](dataframes.md) | Filter, search, and transform dataframes without code |
| [Plotting](plotting.md) | Send plot selections to Python |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/working_with_data/dataframes.md
```md
# Interactive dataframes
**marimo makes you more productive when working with dataframes**.
- [Display dataframes](#displaying-dataframes) in a rich, interactive table and chart views
- [Transform dataframes](#transforming-dataframes) with filters, groupbys,
aggregations, and more, **no code required**
- [Select data](#selecting-dataframes) from tables or charts and get selections
back in Python as dataframes
_marimo integrates with [Pandas](https://pandas.pydata.org/) and
[Polars](https://pola.rs) dataframes natively_.
## Displaying dataframes
marimo lets you page through, search, sort, and filter dataframes, making it
extremely easy to get a feel for your data.
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/docs-df.mp4" type="video/mp4">
<source src="/_static/docs-df.webm" type="video/webm">
</video>
<figcaption>marimo brings dataframes to life.</figcaption>
</figure>
Display dataframes by including them in the last expression of the
cell, just like any other object.
/// tab | pandas
```python
import pandas as pd
df = pd.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
df
```
///
/// tab | polars
```python
import polars as pl
df = pl.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
df
```
///
To opt out of the rich dataframe viewer, use [`mo.plain`][marimo.plain]:
/// tab | pandas
```python
df = pd.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
mo.plain(df)
```
///
/// tab | polars
```python
df = pl.read_json(
"https://raw.githubusercontent.com/vega/vega-datasets/master/data/cars.json"
)
mo.plain(df)
```
///
## Transforming dataframes
### No-code transformations
Use [`mo.ui.dataframe`][marimo.ui.dataframe] to interactively
transform a dataframe with a GUI, no coding required. When you're done, you
can copy the code that the GUI generated for you and paste it into your
notebook.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-dataframe-transform.webm">
</video>
<figcaption>Build transformations using a GUI</figcaption>
</figure>
</div>
/// tab | pandas
```python
# Cell 1
import marimo as mo
import pandas as pd
df = pd.DataFrame({"person": ["Alice", "Bob", "Charlie"], "age": [20, 30, 40]})
transformed_df = mo.ui.dataframe(df)
transformed_df
```
```python
# Cell 2
# transformed_df.value holds the transformed dataframe
transformed_df.value
```
///
/// tab | polars
```python
# Cell 1
import marimo as mo
import polars as pl
df = pl.DataFrame({"person": ["Alice", "Bob", "Charlie"], "age": [20, 30, 40]})
transformed_df = mo.ui.dataframe(df)
transformed_df
```
```python
# Cell 2
# transformed_df.value holds the transformed dataframe
transformed_df.value
```
///
<div align="center">
<figure>
<img src="/_static/docs-dataframe-transform-code.png"/>
<figcaption>Copy the code of the transformation</figcaption>
</figure>
</div>
### Custom filters
Create custom filters with marimo UI elements, like sliders and dropdowns.
/// tab | pandas
```python
# Cell 1 - create a dataframe
df = pd.DataFrame({"person": ["Alice", "Bob", "Charlie"], "age": [20, 30, 40]})
```
```python
# Cell 2 - create a filter
age_filter = mo.ui.slider(start=0, stop=100, value=50, label="Max age")
age_filter
```
```python
# Cell 3 - display the transformed dataframe
filtered_df = df[df["age"] < age_filter.value]
mo.ui.table(filtered_df)
```
///
/// tab | polars
```python
import marimo as mo
import polars as pl
df = pl.DataFrame({
"name": ["Alice", "Bob", "Charlie", "David"],
"age": [25, 30, 35, 40],
"city": ["New York", "London", "Paris", "Tokyo"]
})
age_filter = mo.ui.slider.from_series(df["age"], label="Max age")
city_filter = mo.ui.dropdown.from_series(df["city"], label="City")
mo.hstack([age_filter, city_filter])
```
```python
# Cell 2
filtered_df = df.filter((pl.col("age") <= age_filter.value) & (pl.col("city") == city_filter.value))
mo.ui.table(filtered_df)
```
///
## Select dataframe rows {#selecting-dataframes}
Display dataframes as interactive, [selectable charts](plotting.md) using
[`mo.ui.altair_chart`][marimo.ui.altair_chart] or
[`mo.ui.plotly`][marimo.ui.plotly], or as a row-selectable table with
[`mo.ui.table`][marimo.ui.table]. Select points in the chart, or select a table
row, and your selection is _automatically sent to Python as a subset of the original
dataframe_.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-dataframe-table.webm">
</video>
<figcaption>Select rows in a table, get them back as a dataframe</figcaption>
</figure>
</div>
/// tab | pandas
```python
# Cell 1 - display a dataframe
import marimo as mo
import pandas as pd
df = pd.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
table = mo.ui.table(df, selection="multi")
table
```
```python
# Cell 2 - display the selection
table.value
```
///
/// tab | polars
```python
# Cell 1 - display a dataframe
import marimo as mo
import polars as pl
df = pl.DataFrame({"a": [1, 2, 3], "b": [4, 5, 6]})
table = mo.ui.table(df, selection="multi")
table
```
```python
# Cell 2 - display the selection
table.value
```
///
## Example notebook
For a comprehensive example of using Polars with marimo, check out our [Polars example notebook](https://github.com/marimo-team/marimo/blob/main/examples/third_party/polars/polars_example.py).
Run it with:
```bash
marimo edit https://raw.githubusercontent.com/marimo-team/marimo/main/examples/third_party/polars/polars_example.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/publishing/view_outputs_on_github.md
```md
# View Outputs on GitHub
marimo notebooks are stored as pure Python files, in order to
work with Git versioning and the broader Python ecosystem. However, this means
that unlike Jupyter notebooks, by default you cannot see marimo notebook
outputs on GitHub.
If you would like to make outputs viewable on GitHub, you can configure
any given marimo notebook to automatically snapshot its outputs to an
`ipynb` file. The snapshot will be saved to a `__marimo__` directory
in the same folder where the notebook lives, which you can then
push to GitHub.
Enable snapshotting in the notebook settings menu, using the gear icon in the
top right of any notebook.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/working_with_data/sql.md
```md
# Using SQL
marimo lets you can mix and match **Python and SQL**: Use SQL to query
Python dataframes (or databases like SQLite and Postgres), and get
the query result back as a Python dataframe.
To create a SQL cell, you first need to install additional dependencies,
including [duckdb](https://duckdb.org/):
/// tab | install with pip
```bash
pip install "marimo[sql]"
```
///
/// tab | install with uv
```bash
uv pip install "marimo[sql]"
```
///
/// tab | install with conda
```bash
conda install -c conda-forge marimo duckdb polars
```
///
!!! example "Examples"
For example notebooks, check out
[`examples/sql/` on GitHub](https://github.com/marimo-team/marimo/tree/main/examples/sql/).
## Example
In this example notebook, we have a Pandas dataframe and a SQL cell
that queries it. Notice that the query result is returned as a Python
dataframe and usable in subsequent cells.
<iframe src="https://marimo.app/l/38dxkd?embed=true" class="demo xlarge" height="800px" frameBorder="0"> </iframe>
## Creating SQL cells
You can create SQL cells in one of three ways:
1. **Right-click** an "add cell" button ("+" icon) next to a cell and choose "SQL cell"
2. Convert a empty cell to SQL via the cell
context menu
3. Click the SQL button that appears at the bottom of the notebook
<div align="center">
<figure>
<img src="/_static/docs-sql-cell.png"/>
<figcaption>Add SQL Cell</figcaption>
</figure>
</div>
This creates a "**SQL**" cell for you, which is syntactic sugar for Python code.
The underlying code looks like:
```python
output_df = mo.sql(f"SELECT * FROM my_table LIMIT {max_rows.value}")
```
Notice that we have an **`output_df`** variable in the cell. This contains
the query result, and is a Polars DataFrame (if you have `polars` installed) or
a Pandas DataFrame (if you don't). One of them must be installed in order to
interact with the query result.
The SQL statement itself is an f-string, letting you
interpolate Python values into the query with `{}`. In particular, this means
your SQL queries can depend on the values of UI elements or other Python values,
and they are fit into marimo's reactive dataflow graph.
## Reference a local dataframe
You can reference a local dataframe in your SQL cell by using the name of the
Python variable that holds the dataframe. If you have a database connection
with a table of the same name, the database table will be used instead.
<div align="center">
<figure>
<img src="/_static/docs-sql-df.png"/>
<figcaption>Reference a dataframe</figcaption>
</figure>
</div>
Since the output dataframe variable (`_df`) has an underscore, making it private, it is not referenceable from other cells.
## Reference the output of a SQL cell
Defining a non-private (non-underscored) output variable in the SQL cell allows you to reference the resulting dataframe in other Python and SQL cells.
<div align="center">
<figure>
<img src="/_static/docs-sql-http.png"/>
<figcaption>Reference the SQL result</figcaption>
</figure>
</div>
## Querying files, databases, and APIs
In the above example, you may have noticed we queried an HTTP endpoint instead
of a local dataframe. We are not only limited to querying local dataframes; we
can also query files, databases such as Postgres and SQLite, and APIs:
```sql
-- or
SELECT * FROM 's3://my-bucket/file.parquet';
-- or
SELECT * FROM read_csv('path/to/example.csv');
-- or
SELECT * FROM read_parquet('path/to/example.parquet');
```
For a full list you can check out the [duckdb extensions](https://duckdb.org/docs/extensions/overview).
You can also check out our [examples on GitHub](https://github.com/marimo-team/marimo/tree/main/examples/sql).
## Escaping SQL brackets
Our "SQL" cells are really just Python under the hood to keep notebooks as pure Python scripts. By default, we use `f-strings` for SQL strings, which allows for parameterized SQL like which allows for parameterized SQL like `SELECT * from table where value < {min}`.
To escape real `{`/`}` that you don't want parameterized, use double `{{...}}`:
```sql
SELECT unnest([{{'a': 42, 'b': 84}}, {{'a': 100, 'b': NULL}}]);
```
## Connecting to a custom database
marimo supports bringing your own database via a **connection engine** created with a library like [SQLAlchemy](https://docs.sqlalchemy.org/en/20/core/connections.html#basic-usage), [SQLModel](https://sqlmodel.tiangolo.com/tutorial/create-db-and-table/?h=create+engine#create-the-engine), or a [custom DuckDB connection](https://duckdb.org/docs/api/python/overview.html#connection-options). By default, marimo uses the [In-Memory duckdb connection](https://duckdb.org/docs/connect/overview.html#in-memory-database).
First, you need to define the engine as a Python variable in a cell.
marimo will auto-discover the engine and let you select it in a dropdown in the SQL cell.
```python
import sqlalchemy
import sqlmodel
import duckdb
# Create an in-memory SQLite database with SQLAlchemy
sqlite_engine = sqlachemy.create_engine("sqlite:///:memory:")
# Create a Postgres database with SQLModel
postgres_engine = sqlmodel.create_engine("postgresql://username:password@server:port/database")
# Create a DuckDB connection
duckdb_conn = duckdb.connect("file.db")
```
<div align="center">
<figure>
<img width="750" src="/_static/docs-sql-engine-dropdown.png"/>
<figcaption>Choose a custom database connection</figcaption>
</figure>
</div>
## Interactive tutorial
For an interactive tutorial, run
```bash
marimo tutorial sql
```
at your command-line.
## Examples
Check out our [examples on GitHub](https://github.com/marimo-team/marimo/tree/main/examples/sql).
```
File: /Users/morganmcguire/ML/marimo/docs/guides/apps.md
```md
# Run as an app
The marimo CLI lets you run any notebook as an app: `marimo run` lays out
the notebook as an app and starts a web server that hosts the resulting app.
By default, apps are laid out as a concatenation of their outputs, with
code hidden. You can customize the layout using marimo's built-in drag-and-drop
grid editor; you can also choose to include code in the app view.
## CLI
Run marimo notebooks as apps with
```
marimo run notebook.py
```
View the [CLI documentation](../cli.md#marimo-run) for more details.
## Layout
While editing a notebook with `marimo edit`, you can preview the notebook
as an app by clicking the preview button in the bottom-right of the editor.
(You can also use the command palette.)
### Vertical layout
The default layout is the vertical layout: cell outputs are concatenated
vertically and code is hidden. When combined with marimo's [built-in functions
for laying out outputs](../api/layouts/index.md), as well as its configurable
app widths (configure via the notebook settings menu), the vertical layout can
successfully support a wide breadth of application user interfaces.
### Grid layout
If you prefer a drag-and-drop experience over
[programmatic layout](../api/layouts/index.md), consider using marimo's grid
editor for making your apps: with this editor, you simply drag outputs onto a
grid to arrange them on the page.
Enable the grid editor in the app preview, via a dropdown:
<div align="center">
<figure>
<blockquote class="twitter-tweet" data-media-max-width="560">
<p lang="en" dir="ltr">
<a href="https://t.co/DQpstGAmKh">pic.twitter.com/DQpstGAmKh</a>
</p>&mdash; marimo (@marimo_io)
<a href="https://twitter.com/marimo_io/status/1762595771504116221?ref_src=twsrc%5Etfw">February 27, 2024</a>
</blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
</figure>
<figcaption>Grid layout lets you drag and drop outputs to construct your app</figcaption>
</div>
marimo saves metadata about your constructed layout in a `layouts` folder;
make sure to include this folder when sharing your notebook so that others
can reconstruct your layout.
### Slides layout
If you prefer a slideshow-like experience, you can use the slides layout. Enable the slides layout in the app preview, via the same dropdown as above.
Unlike the grid layout, the slides are much less customizable:
- The order of the slides is determined by the order of the cells in the notebook.
- The slides do not support drag-and-drop rearrangement or resizing.
- All outputs are shown and all code is hidden.
If you need more control over the layout, please file an issue on [GitHub](https://github.com/marimo-team/marimo/issues),
so we can properly prioritize this feature.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/working_with_data/plotting.md
```md
# Plotting
marimo supports most major plotting libraries, including Matplotlib, Seaborn,
Plotly, Altair, and HoloViews. Just import your plotting library of choice and
use it as you normally would.
For Altair and Plotly plots, marimo does something special: use
[`mo.ui.altair_chart`][marimo.ui.altair_chart] or
[`mo.ui.plotly`][marimo.ui.plotly] to connect frontend
selections to Python!
!!! important "Reactive plots!"
marimo supports reactive plots via
[`mo.ui.altair_chart`][marimo.ui.altair_chart] and
[`mo.ui.plotly`][marimo.ui.plotly]! Select and
filter with your mouse, and marimo _automatically makes the selected data
available in Python as a Pandas dataframe_!
## Reactive plots! ⚡
!!! warning "Requirements"
Reactive plots currently require Altair or Plotly. Install with `pip install
altair` or `pip install plotly`, depending on which library you are using.
Selections in plotly are limited to scatter plots, treemaps charts, and sunbursts charts, while Altair supports
a larger class of plots for selections.
### Altair
/// marimo-embed
size: large
```python
@app.cell
async def __():
import pandas as pd
import pyodide
import micropip
import json
await micropip.install('altair')
import altair as alt
return
@app.cell
def __():
cars = pd.DataFrame(json.loads(
pyodide.http.open_url('https://vega.github.io/vega-datasets/data/cars.json').read()
))
chart = mo.ui.altair_chart(alt.Chart(cars).mark_point().encode(
x='Horsepower',
y='Miles_per_Gallon',
color='Origin'
))
return
@app.cell
def __():
mo.vstack([chart, mo.ui.table(chart.value)])
return
```
///
Use [`mo.ui.altair_chart`][marimo.ui.altair_chart] to easily
create interactive, selectable plots: _selections you make on the frontend are
automatically made available as Pandas dataframes in Python._
<div align="center">
<figure>
<video autoplay muted loop playsinline width="600px" align="center">
<source src="/_static/docs-intro.mp4" type="video/mp4">
<source src="/_static/docs-intro.webm" type="video/webm">
</video>
</figure>
</div>
Wrap an Altair chart in [`mo.ui.altair_chart`][marimo.ui.altair_chart]
to make it **reactive**: select data on the frontend, access it via the chart's
`value` attribute (`chart.value`).
#### Disabling automatic selection
marimo automatically adds a default selection based on the mark type, however, you may want to customize the selection behavior of your Altair chart. You can do this by setting `chart_selection` and `legend_selection` to `False`, and using `.add_params` directly on your Altair chart.
```python
# Create an interval selection
brush = alt.selection_interval(encodings=["x"])
_chart = (
alt.Chart(traces, height=150)
.mark_line()
.encode(x="index:Q", y="value:Q", color="traces:N")
.add_params(brush) # add the selection to the chart
)
chart = mo.ui.altair_chart(
_chart,
# disable automatic selection
chart_selection=False,
legend_selection=False
)
chart # You can now access chart.value to get the selected data
```
_Reactive plots are just one way that marimo **makes your data tangible**._
#### Example
```python
import marimo as mo
import altair as alt
import vega_datasets
# Load some data
cars = vega_datasets.data.cars()
# Create an Altair chart
chart = alt.Chart(cars).mark_point().encode(
x='Horsepower', # Encoding along the x-axis
y='Miles_per_Gallon', # Encoding along the y-axis
color='Origin', # Category encoding by color
)
# Make it reactive ⚡
chart = mo.ui.altair_chart(chart)
```
```python
# In a new cell, display the chart and its data filtered by the selection
mo.vstack([chart, chart.value.head()])
```
#### Learning Altair
If you're new to **Altair**, we highly recommend exploring the
[Altair documentation](https://altair-viz.github.io/). Altair provides
a declarative, concise, and simple way to create highly interactive and
sophisticated plots.
Altair is based on [Vega-Lite](https://vega.github.io/vega-lite/), an
exceptional tool for creating interactive charts that serves as the backbone
for marimo's reactive charting capabilities.
##### Concepts
!!! warning "Learn by doing? Skip this section!"
This section summarizes the main concepts used by Altair (and Vega-Lite).
Feel free to skip this section and return later.
Our choice to use the Vega-Lite specification was driven by its robust data
model, which is well-suited for data analysis. Some key concepts are summarized
below. (For a more detailed explanation, with examples, we recommend the
[Basic Statistical Visualization](https://altair-viz.github.io/getting_started/starting.html)
tutorial from Altair.)
- **Data Source**: This is the information that will be visualized in the
chart. It can be provided in various formats such as a dataframe, a list of
dictionaries, or a URL pointing to the data source.
- **Mark Type**: This refers to the visual representation used for each data
point on the chart. The options include 'bar', 'dot', 'circle', 'area', and
'line'. Each mark type offers a different way to visualize and interpret the
data.
- **Encoding**: This is the process of mapping various aspects or dimensions of
the data to visual characteristics of the marks. Encodings can be of
different types:
- **Positional Encodings**: These are encodings like 'x' and 'y' that
determine the position of the marks in the chart.
- **Categorical Encodings**: These are encodings like 'color' and 'shape' that
categorize data points. They are typically represented in a legend for easy
reference.
- **Transformations**: These are operations that can be applied to the data
before it is visualized, for example, filtering and aggregation. These
transformations allow for more complex and nuanced visualizations.
**Automatically interactive.**
marimo adds interactivity automatically, based on the mark used and the
encodings. For example, if you use a `mark_point` and an `x` encoding, marimo
will automatically add a brush selection to the chart. If you add a `color`
encoding, marimo will add a legend and a click selection.
#### Automatic Selections
By default [`mo.ui.altair_chart`][marimo.ui.altair_chart]
will make the chart and legend selectable. Depending on the mark type, the
chart will either have a `point` or `interval` ("brush") selection. When using
non-positional encodings (color, size, etc),
[`mo.ui.altair_chart`][marimo.ui.altair_chart] will also
make the legend selectable.
Selection configurable through `*_selection` params in
[`mo.ui.altair_chart`][marimo.ui.altair_chart]. See the [API
docs][marimo.ui.altair_chart] for details.
!!! note
You may still add your own selection parameters via Altair or Vega-Lite.
marimo will not override your selections.
#### Altair transformations
Altair supports a variety of transformations, such as filtering, aggregation, and sorting. These transformations can be used to create more complex and nuanced visualizations. For example, you can use a filter to show only the points that meet a certain condition, or use an aggregation to show the average value of a variable.
In order for marimo's reactive plots to work with transformations, you must install `vegafusion`, as this feature uses `chart.transformed_data` (which requires version 1.4.0 or greater of the `vegafusion` packages).
```bash
# These can be installed with pip using:
pip install "vegafusion[embed]>=1.4.0"
# Or with conda using:
conda install -c conda-forge "vegafusion-python-embed>=1.4.0" "vegafusion>=1.4.0"
```
### Plotly
!!! warning "mo.ui.plotly only supports scatter plots, treemaps charts, and sunbursts charts"
marimo can render any Plotly plot, but [`mo.ui.plotly`][marimo.ui.plotly] only
supports reactive selections for scatter plots, treemaps charts, and sunbursts charts. If you require other kinds of
selection, consider using [`mo.ui.altair_chart`][marimo.ui.altair_chart].
/// marimo-embed
size: large
```python
@app.cell(hide_code=True)
async def __():
import micropip
await micropip.install("pandas")
await micropip.install("plotly")
import plotly.express as px
return micropip, px
@app.cell
def __(px):
plot = mo.ui.plotly(
px.scatter(x=[0, 1, 4, 9, 16], y=[0, 1, 2, 3, 4], width=600, height=300)
)
plot
return plot
@app.cell
def __(plot):
plot.value
return
```
///
Use [`mo.ui.plotly`][marimo.ui.plotly] to create
selectable Plotly plots whose values are sent back to Python on selection.
## matplotlib
To output a matplotlib plot in a cell's output area, include its `Axes` or
`Figure` object as the last expression in your notebook. For example:
```python
plt.plot([1, 2])
# plt.gca() gets the current `Axes`
plt.gca()
```
or
```python
fig, ax = plt.subplots()
ax.plot([1, 2])
ax
```
If you want to output the plot in the console area, use `plt.show()` or
`fig.show()`.
### Interactive plots
To make matplotlib plots interactive, use
[mo.mpl.interactive][marimo.mpl.interactive].
(Matplotlib plots are not yet reactive.)
```
File: /Users/morganmcguire/ML/marimo/docs/guides/exporting.md
```md
# Exporting to HTML and other formats
Export marimo notebooks to other file formats at the command line using
```bash
marimo export
```
## Export to static HTML
### Export from a running notebook
Export the current view your notebook to static HTML via the notebook
menu:
<div align="center">
<figure>
<img src="/_static/docs-html-export.png"/>
<figcaption>Download as static HTML.</figcaption>
</figure>
</div>
Additionally, you can configure individual notebooks to automatically
save as HTML through the notebook menu. These automatic snapshots are
saved to a folder called `__marimo__` in the notebook directory.
### Export from the command line
Export to HTML at the command line:
```bash
marimo export html notebook.py -o notebook.html
```
or watch the notebook for changes and automatically export to HTML:
```bash
marimo export html notebook.py -o notebook.html --watch
```
When you export from the command line, marimo runs your notebook to produce
its visual outputs before saving as HTML.
!!! note "Note"
If any cells error during the export process, the status code will be non-zero. However, the export result may still be generated, with the error included in the output.
Errors can be ignored by appending `|| true` to the command, e.g. `marimo export html notebook.py || true`.
## Export to a Python script
Export to a flat Python script in topological order, so the cells adhere to
their dependency graph.
```bash
marimo export script notebook.py -o notebook.script.py
```
!!! warning "Top-level await not supported"
Exporting to a flat Python script does not support top-level await. If you have
top-level await in your notebook, you can still execute the notebook as a
script with `python notebook.py`.
## Export to markdown
Export to markdown notebook in top to bottom order, so the cells are in the
order as they appear in the notebook.
```bash
marimo export md notebook.py -o notebook.md
```
This can be useful to plug into other tools that read markdown, such as [Quarto](https://quarto.org/) or [MyST](https://myst-parser.readthedocs.io/).
You can also convert the markdown back to a marimo notebook:
```bash
marimo convert notebook.md > notebook.py
```
## Export to Jupyter notebook
Export to Jupyter notebook in topological order, so the cells adhere to
their dependency graph.
```bash
marimo export ipynb notebook.py -o notebook.ipynb
```
## Exporting to PDF, slides, or rst
If you export to a Jupyter notebook, you can leverage various Jupyter ecosystem tools. For PDFs, you will
need to have [Pandoc](https://nbconvert.readthedocs.io/en/latest/install.html#installing-pandoc) and [Tex](https://nbconvert.readthedocs.io/en/latest/install.html#installing-tex) installed. The examples below use `uvx`, which you can obtain by [installing `uv`](https://docs.astral.sh/uv/getting-started/installation/).
```bash
NOTEBOOK=notebook.ipynb
# Convert to PDF using nbconvert
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to pdf $NOTEBOOK
# Convert to web PDF
uvx --with "nbconvert[webpdf]" --from jupyter-core jupyter nbconvert --to webpdf $NOTEBOOK --allow-chromium-download
# Convert to slides
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to slides $NOTEBOOK
# Convert to rst with nbconvert
uvx --with nbconvert --from jupyter-core jupyter nbconvert --to rst $NOTEBOOK
# Generate PNG/PDF of specific cells using nbconvert
uvx --with nbconvert --with jupyter --from jupyter-core jupyter nbconvert --to pdf --execute --stdout $NOTEBOOK \
--TemplateExporter.exclude_input=True
# Use nbconvert programmatically for more control
uv run --with nbconvert python -c "
from nbconvert import PDFExporter
import nbformat
nb = nbformat.read('$NOTEBOOK', as_version=4)
pdf_exporter = PDFExporter()
pdf_data, resources = pdf_exporter.from_notebook_node(nb)
with open('notebook.pdf', 'wb') as f:
f.write(pdf_data)
"
```
You can also use other tools that work with Jupyter notebooks:
- [Quarto](https://quarto.org) - Create beautiful documents, websites, presentations
- [nbgrader](https://nbgrader.readthedocs.io/) - Grade notebook assignments
## Export to WASM-powered HTML
Export your notebook to a self-contained HTML file that runs using WebAssembly:
```bash
# export as readonly, with code locked
marimo export html-wasm notebook.py -o output_dir --mode run
# export as an editable notebook
marimo export html-wasm notebook.py -o output_dir --mode edit
```
The exported HTML file will run your notebook using WebAssembly, making it completely self-contained and executable in the browser. This means users can interact with your notebook without needing Python or marimo installed.
Options:
- `--mode`: Choose between `run` (read-only) or `edit` (allows editing)
- `--output`: Directory to save the HTML and required assets
- `--show-code/--no-show-code`: Whether to initially show or hide the code in the notebook
- `--watch/--no-watch`: Watch the notebook for changes and automatically export
!!! note "Note"
The exported file must be served over HTTP to function correctly - it
cannot be opened directly from the filesystem (`file://`). Your server must
also serve the assets in the `assets` directory, next to the HTML file. For
a simpler publishing experience, publish to [GitHub
Pages](publishing/github_pages.md) or use the [online
playground](publishing/playground.md).
### Testing the export
You can test the export by running the following command in the directory containing your notebook:
```bash
cd path/to/output_dir
python -m http.server
```
### Including data files
See the docs for [mo.notebook_location][marimo.notebook_location] to learn how
to include data files in exported WASM HTML notebooks.
### Publishing to GitHub Pages
After exporting your notebook to WASM HTML, you can publish it to
[GitHub Pages](https://pages.github.com/) for free. See our [guide on
GitHub Pages](publishing/github_pages.md) to learn more.
### Exporting multiple notebooks
In order to export multiple notebooks under the same folder, you can use the following snippet:
```bash
files=("batch_and_form.py" "data_explorer.py")
for file in "${files[@]}"; do
without_extension="${file%.*}"
marimo export html-wasm "$file" -o public/"$without_extension".html --mode run
done
```
Optionally, you can create an `index.html` file in the public directory:
```bash
echo "<html><body><ul>" > public/index.html
for file in "${files[@]}"; do
without_extension="${file%.*}"
echo "<li><a href=\"$without_extension.html\">$without_extension</a></li>" >> public/index.html
done
echo "</ul></body></html>" >> public/index.html
```
## 🏝️ Embed marimo outputs in HTML using Islands
!!! note "Preview"
Islands are an early feature. While the API likely won't change, there are some improvements we'd like to make before we consider them stable.
Please let us know on [GitHub](https://github.com/marimo-team/marimo/issues) if you run into any issues or have any feedback!
marimo islands are a way to embed marimo outputs and/or python code in your HTML that will become interactive when the page is loaded. This is useful for creating interactive blog posts, tutorials, and educational materials, all powered by marimo's reactive runtime.
Check out an [example island-powered document](./island_example.md).
### Generating islands
Use `MarimoIslandGenerator` to generate HTML for islands
!!! example
/// tab | From code blocks
```python
import asyncio
import sys
from marimo import MarimoIslandGenerator
if sys.platform == 'win32':
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
async def main():
generator = MarimoIslandGenerator()
block1 = generator.add_code("import marimo as mo")
block2 = generator.add_code("mo.md('Hello, islands!')")
# Build the app
app = await generator.build()
# Render the app
output = f"""
<html>
<head>
{generator.render_head()}
</head>
<body>
{block1.render(display_output=False)}
{block2.render()}
</body>
</html>
"""
print(output)
# Save the HTML to a file
output_file = "output.html"
with open(output_file, "w", encoding="utf-8") as f:
f.write(output)
if __name__ == '__main__':
asyncio.run(main())
```
///
/// tab | From notebook files
```python
from marimo import MarimoIslandGenerator
# Create the generator from file
generator = MarimoIslandGenerator.from_file("./<notebook-name>.py", display_code=False)
# Generate and print the HTML without building
# This will still work for basic rendering, though without running the cells
html = generator.render_html(include_init_island=False)
print(html)
# Save the HTML to a file
output_file = "output.html"
with open(output_file, "w", encoding="utf-8") as f:
f.write(html)
```
///
Any relevant `.html` that gets generated can be run through the [`development.md`](https://github.com/marimo-team/marimo/blob/main/frontend/islands/development.md) file instructions.
### Islands in action
!!! warning "Advanced topic!"
Islands are an advanced concept that is meant to be a building block for creating integrations with existing tools such as static site generators or documentation tools.
In order to use marimo islands, you need to import the necessary JS/CSS headers in your HTML file, and use our custom HTML tags to define the islands.
```html
<head>
<!-- marimo js/ccs --
<script type="module" src="https://cdn.jsdelivr.net/npm/@marimo-team/islands@<version>/dist/main.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/@marimo-team/islands@<version>/dist/style.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;family=Lora&amp;family=PT+Sans:wght@400;700&amp;display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww"
crossorigin="anonymous"
/>
</head>
<body>
<marimo-island data-app-id="main" data-cell-id="MJUe" data-reactive="true">
<marimo-cell-output>
<span class="markdown">
<span class="paragraph">Hello, islands!</span>
</span>
</marimo-cell-output>
<marimo-cell-code hidden>mo.md('Hello islands 🏝️!')</marimo-cell-code>
</marimo-island>
</body>
```
::: marimo.MarimoIslandGenerator
```
File: /Users/morganmcguire/ML/marimo/docs/guides/expensive_notebooks.md
```md
# Working with expensive notebooks
marimo provides tools to control when cells run. Use these tools to
prevent expensive cells, which may call APIs or take a long time to run, from
accidentally running.
## Stop execution with `mo.stop`
Use [`mo.stop`][marimo.stop] to stop a cell from executing if a condition
is met:
```python
# if condition is True, the cell will stop executing after mo.stop() returns
mo.stop(condition)
# this won't be called if condition is True
expensive_function_call()
```
Use [`mo.stop`][marimo.stop] with
[`mo.ui.run_button()`][marimo.ui.run_button] to require a button press for
expensive cells:
/// marimo-embed
size: medium
```python
@app.cell
def __():
run_button = mo.ui.run_button()
run_button
return
@app.cell
def __():
mo.stop(not run_button.value, mo.md("Click 👆 to run this cell"))
mo.md("You clicked the button! 🎉")
return
```
///
## Configure how marimo runs cells
### Disabling cell autorun
If you habitually work with very expensive notebooks, you can
[disable automatic
execution](../guides/configuration/runtime_configuration.md#on-cell-change). When
automatic execution is disabled, when you run a cell, marimo
marks dependent cells as stale instead of running them automatically.
### Disabling autorun on startup
marimo autoruns notebooks on startup, with `marimo edit notebook.py` behaving
analogously to `python notebook.py`. This can also be disabled through the
[notebook settings](../guides/configuration/runtime_configuration.md#on-startup).
### Disable individual cells
marimo lets you temporarily disable cells from automatically running. This is
helpful when you want to edit one part of a notebook without triggering
execution of other parts. See the
[reactivity guide](../guides/reactivity.md#disabling-cells) for more info.
## Caching
marimo provides two caching utilities to help you manage expensive computations:
1. In-memory caching with [`mo.cache`][marimo.cache]
2. Disk caching with [`mo.persistent_cache`][marimo.persistent_cache]
Both utilities can be used as decorators or context managers.
### In-memory caching
Use [`mo.cache`][marimo.cache] to cache the return values of
expensive functions, based on their arguments:
/// tab | decorator
```python
import marimo as mo
@mo.cache
def compute_predictions(problem_parameters):
# do some expensive computations and return a value
...
```
///
/// tab | context manager
```python
import marimo as mo
with mo.cache("my_cache") as c:
predictions = compute_predictions(problem_parameters):
```
///
When `compute_predictions` is called with a value of
`problem_parameters` it hasn't seen, it will compute the predictions and store
them in an in-memory cache. The next time it is called with the same
parameters, instead of recomputing the predictions, it will return the
previously computed value from the cache.
??? note "Comparison to `functools.cache`"
[`mo.cache`][marimo.cache] is like `functools.cache` but smarter.
`functools` will sometimes evict values from the cache when it doesn't need to.
In particular, consider the case when a cell defining a `@mo.cache`-d function
re-runs due to an ancestor of it running, or a UI element value changing.
`mo.cache` will analyze the dataflow graph to determine whether or not the
decorated function has changed, and if it hasn't, it's cache won't be
invalidated. In contrast, on re-run a `functools` cache is always invalidated,
because `functools` has no knowledge about the structure of marimo's dataflow
graph.
Conversely, [`mo.cache`][marimo.cache] knows to invalidate the cache if
closed over variables change, whereas `functools.cache` doesn't, yielding
incorrect cache hits.
[`mo.cache`][marimo.cache] is slightly slower than `functools.cache`, but
in most applications the overhead is negligible. For performance critical code,
where the decorated function will be called in a tight loop, prefer
`functools.cache`.
### Disk caching
Use [`mo.persistent_cache`][marimo.persistent_cache] to cache variables to
disk. The next time your run your notebook, the cached variables will be loaded
from disk instead of being recomputed, letting you pick up where you left off.
Reserve this for expensive computations that you would like to persist across
notebook restarts. Cached outputs are automatically saved to `__marimo__/cache`.
**Example.**
/// tab | decorator
```python
import marimo as mo
@mo.persistent_cache(name="my_cache")
def compute_predictions(problem_parameters):
# do some expensive computations and return a value
...
```
///
/// tab | context manager
```python
import marimo as mo
with mo.persistent_cache(name="my_cache"):
# This block of code and its computed variables will be cached to disk
# the first time it's run. The next time it's run, `predictions``
# will be loaded from disk.
predictions = compute_predictions(problem_parameters)
...
```
///
Roughly speaking, [`mo.persistent_cache`][marimo.persistent_cache] registers a
cache hit when the cell is not stale, meaning its code hasn't changed and
neither have its ancestors. On cache hit the code block won't execute and
instead variables will be loaded into memory.
## Lazy-load expensive UIs
Lazily render UI elements that are expensive to compute using
`marimo.lazy`.
For example,
```python
import marimo as mo
data = db.query("SELECT * FROM data")
mo.lazy(mo.ui.table(data))
```
In this example, `mo.ui.table(data)` will not be rendered on the frontend until is it in the viewport.
For example, an element can be out of the viewport due to scroll, inside a tab that is not selected, or inside an accordion that is not open.
However, in this example, data is eagerly computed, while only the rendering of the table is lazy. It is possible to lazily compute the data as well: see the next example.
```python
import marimo as mo
def expensive_component():
import time
time.sleep(1)
data = db.query("SELECT * FROM data")
return mo.ui.table(data)
accordion = mo.accordion({
"Charts": mo.lazy(expensive_component)
})
```
In this example, we pass a function to `mo.lazy` instead of a component. This
function will only be called when the user opens the accordion. In this way,
`expensive_component` lazily computed and we only query the database when the
user needs to see the data. This can be useful when the data is expensive to
compute and the user may not need to see it immediately.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/best_practices.md
```md
# Best practices
Here are best practices for writing marimo notebooks.
**Use global variables sparingly.** Keep the number of global variables in your
program small to avoid name collisions. If you have intermediate variables,
encapsulate them in functions or prefix them with an underscore (`_tmp = ...`) to
make them local to a cell.
**Use descriptive names.** Use descriptive variable names, especially for
global variables. This will help you minimize name clashes, and will also
result in better code.
**Use functions.** Encapsulate logic into functions to avoid polluting the
global namespace with
temporary or intermediate variables, and to avoid code duplication.
**Use [`mo.stop`][marimo.stop] to stop execution.** Use [`mo.stop`][marimo.stop]
to stop a cell from running when a condition is true; this is helpful
when working with expensive notebooks. For example, prevent a cell from running
until a button is clicked using [`mo.ui.run_button`][marimo.ui.run_button] and
[`mo.stop`][marimo.stop].
!!! caution "Expensive notebooks"
For more tips on working with expensive notebooks, see the
associated [guide](../guides/expensive_notebooks.md).
**Use Python modules.** If your notebook gets too long, split complex logic
into helper Python modules and import them into your notebook. Use marimo's
built-in [module
reloading](../guides/configuration/runtime_configuration.md#on-module-change)
to automatically bring changes from your modules into your notebook.
**Minimize mutations.** marimo does not track mutations to objects. Try to
only mutate an object in the cell that creates it, or create new objects
instead of mutating existing ones.
??? example "Example"
_Don't_ split up declarations and mutations over multiple cells. For example, _don't
do this:_
```python
l = [1, 2, 3]
```
```python
l.append(new_item())
```
Instead, _do_ **declare and mutate in the same cell**:
```python
l = [1, 2, 3]
...
l.append(new_item())
```
or, if working in multiple cells, **declare a new variable based on the old
one**:
```python
l = [1, 2, 3]
```
```python
extended_list = l + [new_item()]
```
**Don't use state and `on_change` handlers.** Don't use `on_change` handlers
to react to UI interactions. Instead, use marimo's built-in [reactive execution
for interactive elements](../guides/interactivity.md).
**Write idempotent cells.**
Write cells whose outputs and behavior are the same
when given the same inputs (references); such cells are called idempotent. This
will help you avoid bugs and cache expensive intermediate computations.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/island_example.md
```md
# marimo islands 🏝️
<!-- marimo js/ccs -->
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/main.js"
></script>
<link
href="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/style.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<!-- fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;family=Lora&amp;family=PT+Sans:wght@400;700&amp;display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww"
crossorigin="anonymous"
/>
!!! note "Preview"
Islands are an early feature. While the API likely won't change, there are some improvements we'd like to make before we consider them stable.
Please let us know on [GitHub](https://github.com/marimo-team/marimo/issues) if you run into any issues or have any feedback!
> This content below is powered by marimo's reactive runtime. It will become interactive after initializing the marimo runtime.
<hr/>
<marimo-island data-app-id="main" data-cell-id="Hbol" data-reactive="true">
<marimo-cell-output></marimo-cell-output>
<marimo-cell-code hidden>import%20marimo%20as%20mo</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="MJUe" data-reactive="true">
<marimo-cell-output>
<marimo-ui-element object-id="MJUe-0" random-id="0a0beb44-f946-450d-a690-678a45aeb110">
<marimo-slider
data-initial-value="2"
data-label="null"
data-start="0"
data-stop="10"
data-steps="[]"
data-debounce="false"
data-orientation='"horizontal"'
data-show-value="false"
data-full-width="false"
></marimo-slider>
</marimo-ui-element>
</marimo-cell-output>
<marimo-cell-code hidden>slider%20%3D%20mo.ui.slider(0%2C%2010,value=2)%3B%20slider</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="vblA" data-reactive="true">
<marimo-cell-output>
<span class="markdown"><span class="paragraph">Hello, islands! 🏝️🏝️</span></span>
</marimo-cell-output>
<marimo-ui-element object-id="9c045fc3-f483-4024-ad01-cbf8f06cd7b7" random-id="9c045fc3-f483-4024-ad01-cbf8f06cd7b7">
<marimo-code-editor
data-initial-value='"mo.md(f&#x27;Hello, islands! {&#92;"&#92;ud83c&#92;udfdd&#92;ufe0f&#92;" * slider.value}&#x27;)"'
data-label="null"
data-language='"python"'
data-placeholder='""'
data-disabled="false"
></marimo-code-editor>
</marimo-ui-element>
</marimo-island>
<hr style="margin: 20px 0;" />
??? example "See the HTML"
```html
<marimo-island data-app-id="main" data-cell-id="Hbol" data-reactive="true">
<marimo-cell-output></marimo-cell-output>
<marimo-cell-code hidden>import%20marimo%20as%20mo</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="MJUe" data-reactive="true">
<marimo-cell-output>
<marimo-ui-element object-id="MJUe-0" random-id="0a0beb44-f946-450d-a690-678a45aeb110">
<marimo-slider
data-initial-value="2"
data-label="null"
data-start="0"
data-stop="10"
data-steps="[]"
data-debounce="false"
data-orientation='"horizontal"'
data-show-value="false"
data-full-width="false"
></marimo-slider>
</marimo-ui-element>
</marimo-cell-output>
<marimo-cell-code hidden>slider%20%3D%20mo.ui.slider(0%2C%2010,value=2)%3B%20slider</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="vblA" data-reactive="true">
<marimo-cell-output>
<span class="markdown"><span class="paragraph">Hello, islands! 🏝️🏝️</span></span>
</marimo-cell-output>
<marimo-ui-element object-id="9c045fc3-f483-4024-ad01-cbf8f06cd7b7" random-id="9c045fc3-f483-4024-ad01-cbf8f06cd7b7">
<marimo-code-editor
data-initial-value='"mo.md(f&#x27;Hello, islands! {&#92;"&#92;ud83c&#92;udfdd&#92;ufe0f&#92;" * slider.value}&#x27;)"'
data-label="null"
data-language='"python"'
data-placeholder='""'
data-disabled="false"
></marimo-code-editor>
</marimo-ui-element>
</marimo-island>
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/interactivity.md
```md
# Interactive elements
One of marimo's most powerful features is its first-class support for
interactive user interface (UI) elements, or "widgets", created using
[`marimo.ui`](../api/inputs/index.md). **Interacting with a UI element bound to a
global variable automatically runs all cells that reference it.**
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/readme-ui.mp4" type="video/mp4">
<source src="/_static/readme-ui.webm" type="video/webm">
</video>
</figure>
</div>
!!! example "Examples"
See the [API reference](../api/inputs/index.md) or our [GitHub
repo](https://github.com/marimo-team/marimo/tree/main/examples/ui) for
bite-sized examples on using input elements.
## How interactions run cells
Every UI element you make using [`marimo.ui`](../api/inputs/index.md) has a value, accessible via its
`value` attribute. When you interact with a UI element bound to a global
variable, its value is sent back to Python. A single rule determines what
happens next:
!!! important "Interaction rule"
When a UI element assigned to a global variable is interacted with, marimo
automatically runs all cells that reference the variable (but don't define it).
In the clip at the top of this page, interacting with the slider in the
second cell re-runs the third cell (which outputs markdown) because it
references the slider variable `x`. It doesn't re-run the second cell, because
that cell defines `x`.
**For interactions on a UI element to have any effect, the element must be
assigned to a global variable.**
## Displaying UI elements
Display UI elements in the output area above a cell by including them in the
last expression, just like any other object. You can also embed elements
in [markdown][marimo.md] using Python f-strings, like so:
```python3
slider = mo.ui.slider(1, 10)
mo.md(f"Choose a value: {slider})")
```
## Composite elements
Composite elements are advanced elements let you build UI elements out of other
UI elements. The following composite elements are available:
- [`mo.ui.array`][marimo.ui.array]
- [`mo.ui.dictionary`][marimo.ui.dictionary]
- [`mo.ui.batch`][marimo.ui.batch]
- [`mo.ui.form`][marimo.ui.form]
**Arrays and dictionaries.**
Use [`mo.ui.array`][marimo.ui.array] and
[`mo.ui.dictionary`][marimo.ui.dictionary] to logically group together related
elements. These elements are especially useful when a set of UI elements is
only known at runtime (so you can't assign each to a global variable
individually, but can assign them to an array or dictionary).
You can access the elements contained in an array or dictionary using
Pythonic syntax, and embed these elements in other outputs. See their docstrings
for code examples.
**Batch and form.**
Use these powerful elements to group together multiple UI elements into a
single element with custom formatting, and gate the sending of an element's
value on form submission.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/readme-ui-form.webm">
</video>
<figcaption>Use a form to gate value updates on submission</figcaption>
</figure>
</div>
<div align="center">
<figure>
<img src="/_static/array.png" width="700px"/>
<figcaption>Use an array to group together elements or create a collection of elements that is determined at runtime</figcaption>
</figure>
</div>
## Building custom UI elements using our plugin API
You can build your own reactive and interactive UI elements using
[anywidget](https://github.com/manzt/anywidget). See [our docs on
building custom UI elements](../guides/integrating_with_marimo/custom_ui_plugins.md) to learn more.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/index.md
```md
# Guides
These guides cover marimo's core concepts.
!!! tip "Learn by doing!"
Prefer a hands-on learning experience? marimo comes packaged with interactive
tutorials that you can launch with `marimo tutorial` at the command line.
| Guide | Description |
| :---------------------------------------------------- | :--------------------------------------------------------- |
| [Running cells](reactivity.md) | Understanding how marimo runs cells |
| [Interactive elements](interactivity.md) | Using interactive UI elements |
| [Visualizing outputs](outputs.md) | Creating markdown, plots, and other visual outputs |
| [Migrating from Jupyter](coming_from/jupyter.md) | Tips for transitioning from Jupyter |
| [Expensive notebooks](expensive_notebooks.md) | Tips for working with expensive notebooks |
| [Working with data](working_with_data/index.md) | Using SQL cells, no-code dataframe, and reactive plots |
| [Package reproducibility](package_reproducibility.md) | Making notebooks reproducible down to the packages |
| [Editor Features](editor_features/index.md) | View variables, dataframe schemas, docstrings, and more |
| [Apps](apps.md) | Running notebooks as apps |
| [Scripts](scripts.md) | Running notebooks as scripts |
| [Tests](testing/index.md) | Running unit tests in notebooks |
| [Export notebooks](exporting.md) | Exporting notebooks to HTML, ipynb, flat scripts, and more |
| [Publish to the web](publishing/index.md) | Edit and publish notebooks on the web |
| [Run notebooks with WebAssembly](wasm.md) | Create notebooks in our online playground |
| [Deploying](deploying/index.md) | Deploying marimo notebooks and apps |
| [Configuration](configuration/index.md) | Configure various settings |
| [Coming from other tools](coming_from/index.md) | Transitioning from Jupyter and other tools |
| [Extending marimo](integrating_with_marimo/index.md) | Rich displays of objects, custom UI plugins |
| [State management](state.md) | Advanced: mutable reactive state |
| [Best practices](best_practices.md) | Best practices to help you get the most out of marimo |
| [Troubleshooting](troubleshooting.md) | Troubleshooting notebooks |
```
File: /Users/morganmcguire/ML/marimo/docs/guides/outputs.md
```md
# Visualizing outputs
The last expression of a cell is its visual output, rendered above the cell.
Outputs are included in the "app" or read-only view of the notebook. marimo
comes out of the box a number of elements to help you make rich outputs,
documented in the [API reference](../api/index.md).
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/outputs.webm">
</video>
</figure>
</div>
## Markdown
Markdown is written with the marimo library function [`mo.md`][marimo.md].
Writing markdown programmatically lets you make dynamic markdown: interpolate
Python values into markdown strings, conditionally render your markdown, and
embed markdown in other objects.
Here's a simple hello world example:
```python
import marimo as mo
```
```python
name = mo.ui.text(placeholder="Your name here")
mo.md(
f"""
Hi! What's your name?
{name}
"""
)
```
```python
mo.md(
f"""
Hello, {name.value}!
"""
)
```
Notice that marimo knows how to render marimo objects in markdown: you can just
embed them in [`mo.md()`][marimo.md] using an f-string, and marimo will
figure out how to display them!
For other objects, like matplotlib plots, wrap
them in [`mo.as_html()`][marimo.as_html] to tap into marimo's
media viewer:
```python
mo.md(
f"""
Here's a plot!
{mo.as_html(figure)}
"""
)
```
### Markdown editor
marimo automatically renders cells that only use `mo.md("")`, without an
`f`-string, in a markdown editor that supports common hotkeys.
Because the Markdown editor doesn't support f-strings, you'll need to use
`mo.md` directly to interpolate Python values into your Markdown. You can
switch between the Markdown and Python editors by clicking the button in the
top right.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-markdown-toggle.webm">
</video>
<figcaption>marimo is pure Python, even when you're using markdown.</figcaption>
</figure>
</div>
### Markdown extensions
#### Details
Create expandable details with additional context:
```markdown
/// details | Heads up
Here's some additional context.
///
```
/// marimo-embed-file
filepath: examples/markdown/details.py
///
#### Admonitions
Highlight text using admonitions:
```markdown
/// attention | This is important.
Pay attention to this text!
///
```
/// marimo-embed-file
filepath: examples/markdown/admonitions.py
///
#### Emoji
Use `:emoji:` syntax to add emojis; for example, `:rocket:` creates 🚀.
### Static files
marimo supports serving static files from a `public/` folder located next to your notebook. This is useful for including images or other static assets in your notebook.
To use files from the public folder, create a `public` directory next to your notebook and reference files using the `public/` path prefix:
```python
mo.md(
'''
<img src="public/image.png" width="100" />
or
![alt text](public/image.png)
'''
)
```
For security reasons:
- Only files within the `public` directory can be accessed
- Symlinks are not followed
- Path traversal attempts (e.g., `../`) are blocked
## Layout
The marimo library also comes with elements for laying out outputs, including
[`mo.hstack`][marimo.hstack], [`mo.vstack`][marimo.vstack],
[`mo.accordion`][marimo.accordion], [`mo.ui.tabs`][marimo.ui.tabs], [`mo.sidebar`][marimo.sidebar],
[`mo.nav_menu`][marimo.nav_menu], [`mo.ui.table`][marimo.ui.table],
and [many more](../api/layouts/index.md).
## Progress bars
Use [`mo.status.progress_bar`][marimo.status.progress_bar] and
[`mo.status.spinner`][marimo.status.spinner] to create progress indicators:
```python
# mo.status.progress_bar is similar to TQDM
for i in mo.status.progress_bar(range(10)):
print(i)
```
## Media
marimo comes with functions to display media, including images, audio,
video, pdfs, and more. See the [API docs](../api/media/index.md) for more info.
## Imperatively adding outputs
While a cell's output is its last expression, it can at times be helpful
to imperatively add to the output area while a cell is running. marimo
provides utility functions like
[`mo.output.append`][marimo.output.append] for accomplishing this; see the
[API docs](../api/outputs.md) for more information.
## Console Outputs
Console outputs, such as print statements, show up below a cell in the console
output area; they are not included in the output area or app view by default.
To include console outputs in the cell output area, use
[`mo.redirect_stdout`][marimo.redirect_stdout] or
[`mo.redirect_stderr`][marimo.redirect_stderr]:
```python
with mo.redirect_stdout():
print("Hello, world!")
```
marimo also includes utility functions for [capturing standard out][marimo.capture_stdout] and [standard error][marimo.capture_stderr] without redirecting them. See the [API docs](../api/outputs.md#console-outputs) for more.
## Threading
To create a thread that can reliably communicate outputs to the frontend,
use [`mo.Thread`][marimo.Thread], which has exactly the same API as
as `threading.Thread`.
If you need to forward outputs from threads spawned by third-party code, try
patching `threading.Thread`:
```python
import threading
import marimo as mo
threading.Thread = mo.Thread
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/reactivity.md
```md
# Running cells
marimo _reacts_ to your code changes: run a cell, and all other cells that
refer to the variables it defines are automatically run with the latest data.
This keeps your code and outputs consistent, and eliminates bugs before they
happen.
??? question "Why run cells reactively?"
marimo's "reactive" execution model makes your notebooks more reproducible
by eliminating hidden state and providing a deterministic execution order.
It also powers marimo's support for [interactive
elements](../guides/interactivity.md), for running as apps, and executing as
scripts.
How marimo runs cells is one of the biggest differences between marimo and
traditional notebooks like Jupyter. Learn more at our
[FAQ](../faq.md#faq-jupyter).
!!! tip "Working with expensive notebooks"
marimo provides tools for working with expensive notebooks, in which cells
might take a long time to run or have side-effects.
* The [runtime can be configured](configuration/runtime_configuration.md)
to be **lazy** instead of
automatic, marking cells as stale instead of running them.
* Use [`mo.stop`][marimo.stop] to conditionally
stop execution at runtime.
See [the expensive notebooks guide](expensive_notebooks.md) for more tips.
## How marimo runs cells
marimo statically analyzes each cell (i.e., without running it) to determine
its
- references, the global variables it reads but doesn't define;
- definitions, the global variables it defines.
It then forms a directed acyclic graph (DAG) on cells, with an edge from
one cell to another if the latter references any of the definitions of the
former. When a cell is run, its descendants are marked for execution.
!!! important "Runtime Rule"
When a cell is run, marimo automatically runs all other cells that
**reference** any of the global variables it **defines**.
marimo [does not track mutations](#variable-mutations-are-not-tracked) to
variables, nor assignments to attributes. That means that if you assign an
attribute like `foo.bar = 10`, other cells referencing `foo.bar` will _not_ be
run.
### Execution order
The order cells are executed in is determined by the relationships between
cells and their variables, not by the order of cells on the page (similar
to a spreadsheet). This lets you organize your code in whatever way makes the
most sense to you. For example, you can put helper functions at the bottom of
your notebook.
### Deleting a cell deletes its variables
In marimo, _deleting a cell deletes its global variables from program memory_.
Cells that previously referenced these variables are automatically re-run and
invalidated (or marked as stale, depending on your [runtime
configuration](configuration/runtime_configuration.md)). In this way, marimo
eliminates a common cause of bugs in traditional notebooks like Jupyter.
<!-- <div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-delete-cell.webm">
</video>
<figcaption>No hidden state: deleting a cell deletes its variables.</figcaption>
</figure>
</div> -->
<a name="reactivity-mutations"></a>
### Variable mutations are not tracked
marimo does not track mutations to objects, _e.g._, mutations like
`my_list.append(42)` or `my_object.value = 42` don't trigger reactive re-runs of
other cells. **Avoid defining a variable in one cell and
mutating it in another**.
??? note "Why not track mutations?"
Tracking mutations reliably is impossible in Python. Reacting to mutations
could result in surprising re-runs of notebook cells.
If you need to mutate a variable (such as adding a new column to a dataframe),
you should perform the mutation in the same cell as the one that defines it,
or try creating a new variable instead.
??? example "Create new variables, don't mutate existing ones"
=== "Do this ..."
```python
l = [1]
```
```python
extended_list = l + [2]
```
=== "... not this"
```python
l = [1]
```
```python
l.append(2)
```
??? example "Mutate variables in the cells that define them"
=== "Do this ..."
```python
df = pd.DataFrame({"my_column": [1, 2]})
df["another_column"] = [3, 4]
```
=== "... not this"
```python
df = pd.DataFrame({"my_column": [1, 2]})
```
```python
df["another_column"] = [3, 4]
```
## Global variable names must be unique
**marimo requires that every global variable be defined by only one cell.**
This lets marimo keep code and outputs consistent.
!!! tip "Global variables"
A variable can refer to any Python object. Functions, classes, and imported
names are all variables.
This rule encourages you to keep the number of global variables in your
program small, which is generally considered good practice.
### Creating temporary variables
marimo provides two ways to define temporary variables, which can
help keep the number of global variables in your notebook small.
#### Creating local variables
Variables prefixed with an underscore (_e.g._, `_x`) are "local" to a
cell: they can't be read by other cells. Multiple cells can reuse the same
local variables names.
#### Encapsulating code in functions
If you want most or all the variables in a cell to be temporary, prefixing each
variable with an underscore to make it local may feel inconvenient. In these
situations we recommend encapsulating the temporary variables in a function.
For example, if you find yourself copy-pasting the same plotting code across
multiple cells and only tweaking a few parameters, try the following pattern:
```python
def _():
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
ax.plot([1, 2])
return ax
_()
```
Here, the variables `plt`, `fig`, and `ax` aren't added to the globals.
## Configuring how marimo runs cells
Through the notebook settings menu, you can configure how and when marimo runs
cells. In particular, you can disable autorun on startup, disable autorun
on cell execution, and enable a module autoreloader. Read our
[runtime configuration guide](configuration/runtime_configuration.md) to learn more.
## Disabling cells
Sometimes, you may want to edit one part of a notebook without triggering
automatic execution of its dependent cells. For example, the dependent cells
may take a long time to execute, and you only want to iterate on the first part
of a multi-cell computation.
For cases like this, marimo lets you **disable** cells: when a cell is
disabled, it and its dependents are blocked from running.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-disable-cell.webm">
</video>
<figcaption>Disabling a cell blocks it from running.</figcaption>
</figure>
</div>
When you re-enable a cell, if any of the cell's ancestors ran while it was
disabled, marimo will automatically run it.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-enable-cell.webm">
</video>
<figcaption>Enable a cell through the context menu. Stale cells run
automatically.</figcaption>
</figure>
</div>
```
File: /Users/morganmcguire/ML/marimo/docs/guides/package_reproducibility.md
```md
# Package Reproducibility
marimo is the only Python notebook that is reproducible down to the packages,
serializing requirements in notebook files and running notebooks in
sandboxed venvs. This lets you share standalone notebooks without shipping
`requirements.txt` files alongside them, and guarantees your notebooks will
work weeks, months, even years into the future.
To opt-in to package reproducibility, use the `sandbox` flag:
=== "edit"
```bash
marimo edit --sandbox notebook.py
```
=== "run"
```bash
marimo run --sandbox notebook.py
```
=== "new"
```bash
marimo new --sandbox
```
When running with `--sandbox`, marimo:
1. tracks the packages and versions used by your notebook, saving
them in the notebook file;
2. runs in an isolated virtual environment ("sandbox") that only
contains the notebook dependencies.
marimo's sandbox provides two key benefits. (1) Notebooks that carry their own
dependencies are easy to share — just send the `.py` file. (2) Isolating a
notebook from other installed packages prevents obscure bugs.
!!! note "Requires uv"
Sandboxed notebooks require the uv package manager
([installation
instructions](https://docs.astral.sh/uv/getting-started/installation/)).
!!! tip "Solving the notebook reproducibility crisis"
marimo's support for package sandboxing is only possible because marimo
notebooks are stored as pure Python files, letting marimo take advantage
of new Python standards like [PEP
723](https://peps.python.org/pep-0723/) and tools like uv. In contrast,
traditional notebooks like Jupyter are stored as JSON files, and which suffer
from a [reproducibility
crisis](https://leomurta.github.io/papers/pimentel2019a.pdf) due to the lack
of package management.
## Inline script metadata { #auto-tracking-inline-script-metadata }
When running with `--sandbox`, marimo automatically tracks package metadata in
your notebook file using inline script metadata, which per [PEP
723](https://peps.python.org/pep-0723/) is essentially a pyproject.toml inlined
as the script's header. This metadata is used to manage the
notebook's dependencies and Python version, and looks something like this:
```python
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pandas==<version>",
# "altair==<version>",
# ]
# ///
```
!!! example "Example notebooks"
The [example
notebooks](https://github.com/marimo-team/marimo/tree/main/examples) in our
GitHub repo were all created using `--sandbox`. Take a look at any of them
for an example of the full script metadata.
### Adding and removing packages
When you import a module, if marimo detects that it is a third-party
package, it will automatically be added to the script metadata. Removing
an import does _not_ remove it from the script metadata (since library
code may still use the package).
Adding packages via the package manager panel will also add packages to script
metadata, and removing packages from the panel will in turn remove them from
the script metadata.
You can also edit the script metadata manually in an editor like VS Code or
neovim.
### Package locations
By default, marimo will look for packages on PyPI. You can edit the script
metadata to look for packages elsewhere, such as on GitHub. Consult the [Python
packaging
documentation](https://packaging.python.org/en/latest/specifications/dependency-specifiers/#examples)
for more information.
## Configuration
Running marimo in a sandbox environment uses `uv` to create an isolated virtual
environment. You can use any of `uv`'s [supported environment
variables](https://docs.astral.sh/uv/configuration/environment/).
#### Choosing the Python version
For example, you can specify the Python version using the `UV_PYTHON` environment variable:
```bash
UV_PYTHON=3.13 marimo edit --sandbox notebook.py
```
#### Other common configuration
Another common configuration is `uv`'s link mode:
```bash
UV_LINK_MODE="copy" marimo edit --sandbox notebook.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/scripts.md
```md
# Run as a script
You can run marimo notebooks as scripts at the command line, just like
any other Python script. For example,
```bash
python my_marimo_notebook.py
```
Running a notebook as a script is useful when your notebook has side-effects,
like writing to disk. Print statements and other console outputs will show
up in your terminal.
You can pass arguments to your notebook at the command-line: see
the [docs page on CLI args](../api/cli_args.md) to learn more.
!!! note "Producing notebook outputs"
To run as a script while also producing HTML of the notebook outputs, use
```bash
marimo export html notebook.py -o notebook.html
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/wasm.md
```md
# WebAssembly Notebooks
marimo lets you execute notebooks _entirely in the browser_,
without a backend executing Python. marimo notebooks that
run entirely in the browser are called WebAssembly notebooks, or WASM notebooks
for short.
!!! tip "Try our online playground"
To create your first WASM notebook, try our online playground
at [marimo.new](https://marimo.new). Read the [playground
docs](publishing/playground.md) to learn more.
WASM notebooks have three benefits compared to notebooks hosted using a
traditional client-server model. WASM notebooks:
1. eliminate the need to install Python, making scientific computing accessible;
2. eliminate the cost and complexity of deploying backend infrastructure, making it easy to share notebooks;
3. eliminate network requests to a remote Python runner, making development feel snappy.
!!! question "When should I use WASM notebooks?"
WASM notebooks are excellent for sharing your work, quickly experimenting
with code and models, doing lightweight data exploration, authoring blog
posts, tutorials, and educational materials, and even building tools. For
notebooks that do heavy computation, [use marimo
locally](../getting_started/index.md) or on a backend.
**Try it!** Try editing the below notebook (your browser, not a backend server, is executing it!)
<iframe src="https://marimo.app/l/upciwv?embed=true" width="100%" height=400 frameBorder="0"></iframe>
_This feature is powered by [Pyodide](https://pyodide.org), a port
of Python to WebAssembly that enables browsers to run Python code._
## Creating WASM notebooks
marimo provides three ways to create and share WASM notebooks:
1. [Export to WASM HTML](exporting.md#export-to-wasm-powered-html),
which you can host on GitHub Pages or self-host. This is great for
publishing companion notebooks for research papers that are automatically
updated on Git push, or for embedding interactive notebooks as part of other
websites.
2. The [online playground](publishing/playground.md), which lets you
create one-off notebooks and share via links, no login required. The
playground is also great for embedding editable notebooks in
documentation.
3. The [Community Cloud](publishing/community_cloud/index.md), which
lets you save a collection of notebook to a workspace (for free!) and share
publicly or privately with sensible URLs.
### From GitHub
marimo provides three ways to share notebooks stored on GitHub as WASM notebooks:
1. Automatically publish to GitHub Pages on git push with [our GitHub action](publishing/github_pages.md).
2. Load a notebook by URL into the online playground (New > Open from URL ...)
3. Load a notebook from GitHub in the [Community Cloud](publishing/community_cloud/index.md).
## Packages
!!! tip "Rendering performance"
To make sure markdown and other elements render quickly: make sure to put
`import marimo as mo` in its own cell, with no other lines of code.
WASM notebooks come with many packages pre-installed, including
NumPy, SciPy, scikit-learn, pandas, and matplotlib; see [Pyodide's
documentation](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)
for a full list.
If you attempt to import a package that is not installed, marimo will
attempt to automatically install it for you. To manually install packages, use
[`micropip`](https://micropip.pyodide.org/en/stable/project/usage.html):
In one cell, import micropip:
```python
import micropip
```
In the next cell, install packages:
```python
await micropip.install("plotly")
import plotly
```
### Supported packages
All packages with pure Python wheels on PyPI are supported, as well as
additional packages like NumPy, SciPy, scikit-learn, duckdb, polars, and more.
For a full list of supported packages, see [Pyodide's
documentation on supported packages.](https://pyodide.org/en/stable/usage/packages-in-pyodide.html)
If you want a package to be supported, consider [filing an issue](https://github.com/pyodide/pyodide/issues/new?assignees=&labels=new+package+request&projects=&template=package_request.md&title=).
## Including data
**For notebooks exported to WASM HTML.**
To include data files in notebooks [exported to WASM
HTML](exporting.md#export-to-wasm-powered-html), place them
in a `public/` folder in the same directory as your notebook. When you
export to WASM HTML, the public folder will be copied to the export directory.
In order to access data both locally and when an exported notebook runs via
WebAssembly (e.g., hosted on GitHub Pages), use
[`mo.notebook_location()`][marimo.notebook_location] to construct the path to
your data:
```python
import polars as pl
path_to_csv = mo.notebook_location() / "public" / "data.csv"
df = pl.read_csv(str(path_to_csv))
df.head()
```
**Fetching data files from the web.**
Instead of bundling data files with your notebook, you can host data files on
the web and fetch them in your notebook. Depending on where your files are
hosted, you may need to use a CORS Proxy; see the [Pyodide
documentation](https://pyodide.org/en/stable/usage/loading-packages.html#installing-wheels-from-arbitrary-urls)
for more details.
**Playground notebooks.** When opening a playground
notebook from GitHub, all the files in the GitHub repo are made available to
your notebook. See the [Playground
Guide](publishing/playground.md#including-data-files) for more info.
**Community Cloud notebooks.** Our free [Community
Cloud](publishing/community_cloud/index.md) lets you upload a limited
amount of data, and also lets you sync notebooks (and their data) from GitHub.
## Limitations
While WASM notebooks let you share marimo notebooks seamlessly, they have some
limitations.
**PDB.** PDB is not currently supported.
**Threading and multi-processing.** WASM notebooks do not support multithreading
and multiprocessing. [This may be fixed in the future](https://github.com/pyodide/pyodide/issues/237).
**Memory.** WASM notebooks have a memory limit of 2GB; this may be increased
in the future. If memory consumption is an issue, try offloading memory-intensive
computations to hosted APIs or precomputing expensive operations.
## Browser support
WASM notebooks are supported in the latest versions of Chrome, Firefox, Edge, and Safari.
Chrome is the recommended browser for WASM notebooks as it seems to have the
best performance and compatibility.
```
File: /Users/morganmcguire/ML/marimo/docs/guides/troubleshooting.md
```md
# Troubleshooting
This guide covers common issues and unexpected behaviors you might encounter when using marimo notebooks, along with ways to debug and resolve them. If your issue isn't covered here, try checking our [FAQ](../faq.md).
## Why aren't my cells running?
If you're expecting cells to run in response to changes in other cells, but they're not, consider the following:
### Check for mutations
marimo doesn't track mutations to objects. If you're modifying an object in one cell and expecting another cell to react, this won't work as expected.
Instead of mutating objects across cells, try creating new objects or performing all mutations within the same cell.
[Read more about reactivity](../guides/reactivity.md).
### Verify cell connections
Use the Dependency Panel or Variable Panel to check if your cells are actually connected as you expect.
1. Open the Dependency Panel (graph icon) or Variable Panel (variable icon) in the left sidebar.
2. Look for arrows connecting your cells or check which cells are listed as using each variable.
3. If connections are missing, review your variable usage to ensure cells are properly referencing each other.
<div align="center">
<figure>
<img src="/_static/docs-dependency-graph.png"/>
<figcaption>
Dependency graph showing cell connections.
</figcaption>
</figure>
</div>
## Why is my cell running unexpectedly?
If a cell is running more often than you anticipate:
### Check cell dependencies
Use the Dependency Panel or Variable Panel to see what's triggering your cell:
1. Open the Dependency Panel or Variable Panel.
2. Locate your cell and examine its incoming connections.
3. You might find unexpected dependencies that are causing the cell to run.
### Understand global vs local variables vs functions args
Ensure you're not inadvertently using a global variables when intending to use a local variable or function argument:
1. Check for any variables used in your cell that aren't defined within it.
2. Consider using local variables (prefixed with `_`) for values that shouldn't be consumed by other cells.
## Why is my UI element's value being reset?
If a UI element's value keeps resetting:
### Check that cell defining the UI element isn't rerunning
If the cell defining the UI element reruns, it will reset the element's value to its initial `value` argument. You may be able to avoid this by splitting the UI element definition into a separate cell.
### Use state for persistence
If you need to maintain UI element values across cell runs, consider using `mo.state`:
```python
# Declare state in a separate cell
get_value, set_value = mo.state(initial_value)
```
```python
element = mo.ui.slider(0, 10, value=get_value(), on_change=set_value)
```
This way, the value persists even if the cell defining the element reruns.
## How can I force one cell to run after another?
If you need to ensure a specific execution order:
### Use explicit dependencies
Create an explicit dependency by using a variable from the first cell in the second:
```python
# Cell 1
result = some_computation()
```
```python
# Cell 2
_ = result # This creates a dependency on Cell 1
further_computation()
```
### Consider refactoring
If you find yourself needing to force execution order often, it might be a sign that your notebook structure could be improved:
1. Try to organize your cells so that natural data flow creates the desired order.
2. Consider combining related operations into single cells where appropriate.
## General debugging tips
- Use the Variables Panel to inspect variable values and see where they're defined and used.
- Add print statements or use `mo.md()` to output debug information in cell outputs.
- Temporarily disable cells to isolate issues.
- Use the "Lazy" runtime configuration to see which cells are being marked as stale without automatically running them.
Remember, marimo's reactivity is based on global variable definitions and references, and mutations to objects aren't tracked. Keeping this in mind can help you understand and debug unexpected behaviors in your notebooks.
## Why is the notebook returning 404s on the web assets?
If you're seeing 404 errors for web assets like JS or CSS files, it may be due to symlink settings or proxy settings.
### Check symlink settings
If you are using `bazel` or `uv`'s [**link-mode: symlink**](https://docs.astral.sh/uv/reference/settings/#link-mode), you may need to adjust your symlink settings to ensure that web assets are correctly found. By default marimo does not follow symlinks, so you may need to turn this setting on.
Locate your `marimo.toml` configuration file with `marimo config show`, and edit the `follow_symlink` flag:
```toml title="marimo.toml"
[server]
follow_symlink = true
```
### Check proxy settings
If you are using a proxy server, you need to include the `--proxy` flag when running marimo. The proxy will default to port 80 if no port is specified. For example, if your proxy is `example.com` and it uses port 8080, you would run:
```bash
marimo edit --proxy example.com:8080
# or
marimo run --proxy example.com:8080
```
```
File: /Users/morganmcguire/ML/marimo/docs/guides/state.md
```md
# Reactive state
!!! warning "Stop! Read the interactivity guide first!"
**Read the guide on [creating interactive
elements](../guides/interactivity.md)** before reading this one!
!!! warning "Advanced topic!"
This guide covers reactive state (`mo.state`), an advanced topic.
**You likely don't need `mo.state`**. UI elements already have built-in
state, their associated value, which you can access with their `value` attribute.
For example, `mo.ui.slider()` has a value that is its current position on an
interval, while `mo.ui.button()` has a value that can be configured to
count the number of times it has been clicked, or to toggle between `True` and
`False`. Additionally, interacting with UI elements bound to global variables
[automatically executes cells](../guides/interactivity.md) that reference those
variables, letting you react to changes by just reading their
`value` attributes. **This functional paradigm is the preferred way of
reacting to UI interactions in marimo.** **Chances are, the reactive
execution built into UI elements will suffice.** (For example, [you don't need
reactive state to handle a button click](../recipes.md#working-with-buttons).)
That said, here are some signs you might need `mo.state`:
- you need to maintain historical state related to a UI element that can't
be computed from its built-in `value` (_e.g._, all values the user has
ever input into a form)
- you need to synchronize two different UI elements (_e.g._, so that
interacting with either one controls the other)
- you need to introduce cycles across cells
**In over 99% of cases, you don't need and shouldn't use `mo.state`.** This
feature can introduce hard-to-find bugs.
You can build powerful, interactive notebooks and apps using just `mo.ui` and
reactivity.
But sometimes, you might want interactions to mutate state:
- You're building a checklist, and you want to maintain a list of action
items, even as you add and remove some items.
<div align="center" style="margin-top:2rem; margin-bottom:2rem">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/docs-state-task-list.mp4" type="video/mp4">
<source src="/_static/docs-state-task-list.webm" type="video/webm">
</video>
</figure>
<figcaption>A proof-of-concept TODO list made using state.</figcaption>
</div>
- You want to tie two different UI elements so that updating **either** one
updates the other.
<div align="center" style="margin-top:2rem; margin-bottom:2rem">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-state-tied.webm">
</video>
<figcaption>Use state to tie two elements together in a cycle.</figcaption>
</figure>
</div>
!!! warning "Use reactive execution for uni-directional flow"
If you just want the value of a single element to update another element,
then **you shouldn't use `mo.state`**. Instead, use marimo's built-in
reactive execution --- see the [interactivity guide](../guides/interactivity.md).
For cases like these, marimo provides the function [`mo.state()`](../api/state.md),
which creates a state object and returns a getter and setter function. When you
call the setter function in one cell, all other cells that reference the getter
function **via a global variable** are automatically run (similar to UI
elements).
!!! note "State and UI elements are similar"
State is analogous to UI elements. When you interact
with a UI element, all cells that reference that element via a global variable
run automatically with the new value. In the same way, when you update state
via the setter, all other cells that reference the getter via
a global variable run automatically with the new value.
[`mo.state()`](../api/state.md) takes an initial state value as its argument, creates
a state object, and returns
- a getter function for reading the state
- a setter function for updating the state
For exaxmple,
```python
get_counter, set_counter = mo.state(0)
```
!!! attention "Assign state to global variables!"
When using `mo.state()`, **you must assign the state getter to a global
variable**. This is similar to UI elements work.
## Reading state
Access the state's latest value via the getter:
```python
get_counter()
```
## Updating state
You can update a state's value by calling its setter function with a new value.
For example,
```python
set_counter(1)
```
To update the state based on its current value, pass a function that takes
the current state value as an argument and returns a new value
```python
set_counter(lambda count: count + 1)
```
A single rule determines what happens next:
!!! tip "State reactivity rule"
When a state setter function is called in one cell, marimo
automatically runs all _other_ cells that reference any **global** variables
assigned to the state getter.
This rule has some important aspects:
1. Only cells that read the state getter via a global variable will be run.
2. The cell that called the setter won't be re-run, even if it references
the getter. This restriction helps prevent against bugs that could
otherwise arise. To lift this restriction, and allow the caller cell
to be re-run, create your state with `mo.state(value, allow_self_loops=True)`.
Notice how similar this rule is to the reactivity rule for UI element
interactions.
## Using state with UI elements
Every UI element takes an optional `on_change` callback, a function that takes
the new value of the element and does anything with it. You can use the setter
function in an `on_change` callback to mutate state.
!!! note "Use state sparingly"
You can get far using just `mo.ui`, without state, because marimo
automatically runs cells that reference UI elements on interaction
(see the [interactivity guide](../guides/interactivity.md)). Only
use `on_change` callbacks as a last resort!
### Example: counter
The next few cells implement a counter controlled by two buttons. This
particular example could be implemented without state (try it!), but the
implementation using state is simpler.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-state-counter.webm">
</video>
</figure>
</div>
```python
import marimo as mo
```
```python
get_counter, set_counter = mo.state(0)
increment = mo.ui.button(
label="increment",
on_change=lambda _: set_counter(lambda v: v + 1),
)
decrement = mo.ui.button(
label="decrement",
on_change=lambda _: set_counter(lambda v: v - 1),
)
mo.hstack([increment, decrement], justify="center")
```
```python
mo.md(
f"""
The counter's current value is **{get_counter()}**!
This cell runs automatically on button click, even though it
doesn't reference either button.
"""
)
```
### Example: tied elements
This example shows how to tie two different UI elements so that each one's
value depends on the other. This is impossible to do without `mo.state`.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-state-tied.webm">
</video>
</figure>
</div>
```python
import marimo as mo
```
```python
get_x, set_x = mo.state(0)
```
```python
x = mo.ui.slider(
0, 10, value=get_x(), on_change=set_x, label="$x$:"
)
```
```python
x_plus_one = mo.ui.number(
1,
11,
value=get_x() + 1,
on_change=lambda v: set_x_state(v - 1),
label="$x + 1$:",
)
```
```python
[x, x_plus_one]
```
!!! note "Create tied UI elements in separate cells"
Notice that we created the slider and number elements in different cells.
When tying elements, this is necessary, because calling a setter
in a cell queues all _other_ cells reading the state to run, not including
the one that just called the setter.
!!! warning "Cycles at runtime"
You can use state to introduce cycles across cells at runtime. This lets
you tie multiple UI elements together, for example. Just be careful not to
introduce an infinite loop!
marimo programs are statically parsed into directed acyclic graphs (DAGs)
involving cells, and state doesn't change that. Think of state setters
as hooking into the DAG: at runtime, when they're invoked (and only when
they're invoked), they trigger additional computation.
### Example: todo list
The next few cells use state to create a todo list.
<div align="center">
<figure>
<video autoplay muted loop playsinline width="100%" height="100%" align="center" src="/_static/docs-state-task-list.webm">
</video>
</figure>
</div>
```python
import marimo as mo
from dataclasses import dataclass
```
```python
@dataclass
class Task:
name: str
done: bool = False
get_tasks, set_tasks = mo.state([])
task_added, set_task_added = mo.state(False)
```
```python
# Refresh the text box whenever a task is added
task_added
task_entry_box = mo.ui.text(placeholder="a task ...")
```
```python
def add_task():
if task_entry_box.value:
set_tasks(lambda v: v + [Task(task_entry_box.value)])
set_task_added(True)
def clear_tasks():
set_tasks(lambda v: [task for task in v if not task.done])
add_task_button = mo.ui.button(
label="add task",
on_change=lambda _: add_task(),
)
clear_tasks_button = mo.ui.button(
label="clear completed tasks",
on_change=lambda _: clear_tasks()
)
```
```python
task_list = mo.ui.array(
[mo.ui.checkbox(value=task.done, label=task.name) for task in get_tasks()],
label="tasks",
on_change=lambda v: set_tasks(
lambda tasks: [Task(task.name, done=v[i]) for i, task in enumerate(tasks)]
),
)
```
```python
mo.hstack(
[task_entry_box, add_task_button, clear_tasks_button], justify="start"
)
```
```
File: /Users/morganmcguire/ML/marimo/docs/integrations/google_sheets.md
```md
# Google Sheets
## Getting Started
To use Google Sheets as a data source, you will need to install the `gspread` and `oauth2client` Python packages. You can install this package using `pip`:
```bash
pip install gspread oauth2client
```
## Authentication
### Application Default Credentials (Recommended)
The easiest way to authenticate with Google Sheets is to use [Application Default Credentials](https://cloud.google.com/docs/authentication/production). If you are running marimo on Google Cloud and your resource has a service account attached, then Application Default Credentials will automatically be used.
If you are running marimo locally, you can authenticate with Application Default Credentials by running the following command:
```bash
gcloud auth application-default login
```
### Service Account Key File
To authenticate with Google Sheets, you will need to create a service account and download the service account key file. You can create a service account and download the key file by following the instructions [here](https://cloud.google.com/iam/docs/creating-managing-service-account-keys).
Once you have downloaded the key file, you can authenticate with Google Sheets by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of the key file:
```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key/file.json
```
## Reading Data
To read data from Google Sheets, you will need to authenticate and create a `gspread.Client`. You can then use this object to read data from Google Sheets.
```python
# Cell 1 - Load libraries
import marimo as mo
import pandas as pd
import os
import gspread
from oauth2client.service_account import ServiceAccountCredentials
# Authenticate with Google Sheets
scope = [
"https://spreadsheets.google.com/feeds",
"https://www.googleapis.com/auth/drive",
]
credentials = ServiceAccountCredentials.from_json_keyfile_name(
os.environ["GOOGLE_APPLICATION_CREDENTIALS"], scope
)
gc = gspread.authorize(credentials)
# Cell 2 - Load the sheet
wks = gc.open("marimo").sheet1
mo.ui.table(pd.DataFrame(wks.get_all_records()))
```
## Example
Check out our full example using Google Sheets [here](https://github.com/marimo-team/marimo/blob/main/examples/cloud/gcp/google_sheets.py)
Or run it yourself:
```bash
marimo run https://raw.githubusercontent.com/marimo-team/marimo/main/examples/cloud/gcp/google_sheets.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/integrations/google_cloud_storage.md
```md
# Google Cloud Storage
## Getting Started
To use Google Cloud Storage as a data source, you will need to install the `google-cloud-storage` Python package. You can install this package using `pip`:
```bash
pip install google-cloud-storage
```
## Authentication
### Application Default Credentials (Recommended)
The easiest way to authenticate with Google Cloud Storage is to use [Application Default Credentials](https://cloud.google.com/docs/authentication/production). If you are running marimo on Google Cloud and your resource has a service account attached, then Application Default Credentials will automatically be used.
If you are running marimo locally, you can authenticate with Application Default Credentials by running the following command:
```bash
gcloud auth application-default login
```
### Service Account Key File
To authenticate with Google Cloud Storage, you will need to create a service account and download the service account key file. You can create a service account and download the key file by following the instructions [here](https://cloud.google.com/iam/docs/creating-managing-service-account-keys).
Once you have downloaded the key file, you can authenticate with Google Cloud Storage by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of the key file:
```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key/file.json
```
## Reading Data
To read data from Google Cloud Storage, you will need to create a `StorageClient` object. You can then use this object to read data from Google Cloud Storage.
```python
# Cell 1 - Load libraries
import marimo as mo
from google.cloud import storage
# Cell 2 - Load buckets
client = storage.Client()
buckets = client.list_buckets()
# Cell 3 - Select bucket
selected_bucket = mo.ui.dropdown(
label="Select bucket", options=[b.name for b in buckets]
)
selected_bucket
# Cell 4 - Load files
files = list(bucket.list_blobs())
items = [
{
"Name": f.name,
"Updated": f.updated.strftime("%h %d, %Y"),
"Size": f.size,
}
for f in files
]
file_table = mo.ui.table(items, selection="single")
file_table if items else mo.md("No files found").callout()
```
## Example
Check out our full example using Google Cloud Storage [here](https://github.com/marimo-team/marimo/blob/main/examples/cloud/gcp/google_cloud_storage.py)
Or run it yourself:
```bash
marimo run https://raw.githubusercontent.com/marimo-team/marimo/main/examples/cloud/gcp/google_cloud_storage.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/integrations/google_cloud_bigquery.md
```md
# Google Cloud BigQuery
## Getting Started
To use Google Cloud BigQuery as a data source, you will need to install the `google-cloud-bigquery` Python package. You can install this package using `pip`:
```bash
pip install google-cloud-bigquery db-dtypes
```
## Authentication
### Application Default Credentials (Recommended)
The easiest way to authenticate with Google Cloud BigQuery is to use [Application Default Credentials](https://cloud.google.com/docs/authentication/production). If you are running marimo on Google Cloud and your resource has a service account attached, then Application Default Credentials will automatically be used.
If you are running marimo locally, you can authenticate with Application Default Credentials by running the following command:
```bash
gcloud auth application-default login
```
### Service Account Key File
To authenticate with Google Cloud BigQuery, you will need to create a service account and download the service account key file. You can create a service account and download the key file by following the instructions [here](https://cloud.google.com/iam/docs/creating-managing-service-account-keys).
Once you have downloaded the key file, you can authenticate with Google Cloud BigQuery by setting the `GOOGLE_APPLICATION_CREDENTIALS` environment variable to the path of the key file:
```bash
export GOOGLE_APPLICATION_CREDENTIALS=/path/to/key/file.json
```
## Reading Data
To read data from Google Cloud BigQuery, you will need to create a `BigQueryClient` object. You can then use this object to read data from Google Cloud BigQuery.
```python
# Cell 1 - Load libraries
import marimo as mo
from google.cloud import bigquery
# Cell 2 - Load datasets
client = bigquery.Client()
datasets = list(client.list_datasets())
# Cell 3 - Select dataset
selected_dataset = mo.ui.dropdown(
label="Select dataset", options=[d.dataset_id for d in datasets]
)
selected_dataset
# Cell 4 - Load tables
dataset = client.dataset(selected_dataset.value)
tables = list(client.list_tables(dataset))
selected_table = mo.ui.dropdown(
label="Select table", options=[t.table_id for t in tables]
)
selected_table
# Cell 5 - Load table data
results = client.list_rows(dataset.table(selected_table.value), max_results=10)
mo.ui.table(results.to_dataframe(), selection=None)
```
## Example
Check out our full example using Google Cloud BigQuery [here](https://github.com/marimo-team/marimo/blob/main/examples/cloud/gcp/google_cloud_bigquery.py)
Or run it yourself:
```bash
marimo run https://raw.githubusercontent.com/marimo-team/marimo/main/examples/cloud/gcp/google_cloud_bigquery.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/integrations/index.md
```md
# Integrations
It is easy to integrate your preferred data sources or data warehouses with marimo. Since marimo is strictly Python, you can utilize any Python library to access your data. In this section, we provide some examples of how to integrate with popular data sources.
| Integration | Description |
|-------------|-------------|
| [MotherDuck](motherduck.md) | Integrating with MotherDuck |
| [Google Cloud Storage](google_cloud_storage.md) | Integrating with Google Cloud Storage |
| [Google Cloud BigQuery](google_cloud_bigquery.md) | Integrating with Google Cloud BigQuery |
| [Google Sheets](google_sheets.md) | Integrating with Google Sheets |
```
File: /Users/morganmcguire/ML/marimo/docs/overrides/main.html
```html
{% extends "base.html" %} {% block announce %}
<!-- Add your announcement here -->
<span class="twemoji"> {% include ".icons/material/new-box.svg" %} </span>
<strong>New:</strong> Check out our latest features in the <a href="/latest">documentation</a>! {% endblock %} {% block
extrahead %} {{ super() }}
<!-- Add any custom head elements here -->
<meta property="og:type" content="website" />
<meta property="og:site_name" content="Marimo Documentation" />
<meta
property="og:title"
content="{% if page.meta and page.meta.title %}{{ page.meta.title }}{% elif page.title %}{{ page.title }}{% else %}{{ config.site_name }}{% endif %}"
/>
{% endblock %} {% block scripts %} {{ super() }}
<!-- Add any custom scripts here -->
{% endblock %}
```
File: /Users/morganmcguire/ML/marimo/docs/integrations/motherduck.md
```md
# MotherDuck
[MotherDuck](https://motherduck.com/) is a cloud-based data warehouse that combines the power of DuckDB with the scalability of the cloud. This guide will help you integrate MotherDuck with marimo.
## 1. Connecting to MotherDuck
To use MotherDuck as a data source, you'll need to install the `marimo[sql]` Python package.
/// tab | install with pip
```bash
pip install "marimo[sql]"
```
///
/// tab | install with uv
```bash
uv pip install "marimo[sql]"
```
///
/// tab | install with conda
```bash
conda install -c conda-forge marimo duckdb polars
```
///
To connect to MotherDuck, import `duckdb` and `ATTACH` your MotherDuck database.
## Using MotherDuck
### 1. Connecting and Database Discovery
/// tab | SQL
```sql
ATTACH IF NOT EXISTS 'md:my_db'
```
///
/// tab | Python
```python
import duckdb
# Connect to MotherDuck
duckdb.sql("ATTACH IF NOT EXISTS 'md:my_db'")
```
///
You will be prompted to authenticate with MotherDuck when you run the above cell. This will open a browser window where you can log in and authorize your marimo notebook to access your MotherDuck database. In order to avoid being prompted each time you open a notebook, you can set the `motherduck_token` environment variable:
```bash
export motherduck_token="your_token"
marimo edit
```
Once connected, your MotherDuck tables are automatically discovered in the Datasources Panel:
<div align="center">
<figure>
<img src="/_static/motherduck/motherduck_db_discovery.png"/>
<figcaption>Browse your MotherDuck databases</figcaption>
</figure>
</div>
### 2. Writing SQL Queries
You can query your MotherDuck tables using SQL cells in marimo. Here's an example of how to query a table and display the results using marimo:
<div align="center">
<figure>
<img src="/_static/motherduck/motherduck_sql.png"/>
<figcaption>Query a MotherDuck table</figcaption>
</figure>
</div>
marimo's reactive execution model extends into SQL queries, so changes to your SQL will automatically trigger downstream computations for dependent cells (or optionally mark cells as stale for expensive computations).
<video controls width="100%" height="100%" align="center" src="/_static/motherduck/motherduck_reactivity.mp4"> </video>
### 3. Mixing SQL and Python
MotherDuck allows you to seamlessly mix SQL queries with Python code, enabling powerful data manipulation and analysis. Here's an example:
<div align="center">
<figure>
<img src="/_static/motherduck/motherduck_python_and_sql.png"/>
<figcaption>Mixing SQL and Python</figcaption>
</figure>
</div>
This example demonstrates how you can use SQL to query your data, then use Python and marimo to further analyze and visualize the results.
## Example Notebook
For a full example of using MotherDuck with marimo, check out our [MotherDuck example notebook](https://github.com/marimo-team/marimo/blob/main/examples/sql/connect_to_motherduck.py).
```bash
marimo edit https://github.com/marimo-team/marimo/blob/main/examples/sql/connect_to_motherduck.py
```
```
File: /Users/morganmcguire/ML/marimo/docs/cli.md
```md
<style>
td code {
white-space: nowrap;
}
</style>
::: mkdocs-click
:module: marimo._cli.cli
:command: main
:prog_name: marimo
:depth: 0
:style: table
```
File: /Users/morganmcguire/ML/marimo/docs/blocks.py
```py
import textwrap
import xml.etree.ElementTree as etree
from typing import Any, Dict, List, Union, cast
import urllib.parse
from pymdownx.blocks import BlocksExtension # type: ignore
from pymdownx.blocks.block import Block, type_string, type_string_in # type: ignore
class BaseMarimoBlock(Block):
"""Base class for marimo embed blocks"""
OPTIONS: Dict[str, List[Union[str, Any]]] = {
"size": [
"medium",
type_string_in(["small", "medium", "large", "xlarge", "xxlarge"]),
],
"mode": ["read", type_string_in(["read", "edit"])],
}
def on_create(self, parent: etree.Element) -> etree.Element:
container = etree.SubElement(parent, "div")
container.set("class", "marimo-embed-container")
return container
def on_add(self, block: etree.Element) -> etree.Element:
return block
def _create_iframe(self, block: etree.Element, url: str) -> None:
# Clear existing content
block.text = None
for child in block:
block.remove(child)
# Add iframe
size: str = cast(str, self.options["size"])
iframe = etree.SubElement(block, "iframe")
iframe.set("class", f"demo {size}")
iframe.set("src", url)
iframe.set(
"allow",
"camera; geolocation; microphone; fullscreen; autoplay; encrypted-media; picture-in-picture; clipboard-read; clipboard-write",
)
iframe.set("width", "100%")
iframe.set("height", "400px")
iframe.set("frameborder", "0")
iframe.set("style", "display: block; margin: 0 auto;")
def on_markdown(self) -> str:
return "raw"
class MarimoEmbedBlock(BaseMarimoBlock):
NAME: str = "marimo-embed"
OPTIONS: Dict[str, List[Union[str, Any]]] = {
**BaseMarimoBlock.OPTIONS,
"app_width": ["wide", type_string_in(["wide", "full", "compact"])],
}
def on_end(self, block: etree.Element) -> None:
code = block.text.strip() if block.text else ""
if code.startswith("```python"):
code = code[9:]
code = code[:-3]
code = textwrap.dedent(code)
app_width: str = cast(str, self.options["app_width"])
mode: str = cast(str, self.options["mode"])
url = create_marimo_app_url(
code=create_marimo_app_code(code=code, app_width=app_width),
mode=mode,
)
self._create_iframe(block, url)
class MarimoEmbedFileBlock(BaseMarimoBlock):
NAME: str = "marimo-embed-file"
OPTIONS: Dict[str, List[Union[str, Any]]] = {
**BaseMarimoBlock.OPTIONS,
"filepath": ["", type_string],
"show_source": ["true", type_string_in(["true", "false"])],
}
def on_end(self, block: etree.Element) -> None:
filepath = cast(str, self.options["filepath"])
if not filepath:
raise ValueError("File path must be provided")
# Read from project root
try:
with open(filepath, "r") as f:
code = f.read()
except FileNotFoundError:
raise ValueError(f"File not found: {filepath}")
mode: str = cast(str, self.options["mode"])
url = create_marimo_app_url(code=code, mode=mode)
self._create_iframe(block, url)
# Add source code section if enabled
show_source: str = cast(str, self.options.get("show_source", "true"))
if show_source == "true":
details = etree.SubElement(block, "details")
summary = etree.SubElement(details, "summary")
summary.text = f"Source code for `{filepath}`"
# TODO: figure out syntax highlighting
# md_text = f"\n\n```python\n{code}\n```\n\n"
# result = self.md.htmlStash.store(self.md.convert(md_text))
# container.text = result
copy_paste_container = etree.SubElement(details, "p")
copy_paste_container.text = "Tip: paste this code into an empty cell, and the marimo editor will create cells for you"
code_container = etree.SubElement(details, "pre")
code_container.set("class", "marimo-source-code")
code_block = etree.SubElement(code_container, "code")
code_block.set("class", "language-python")
code_block.text = code
def uri_encode_component(code: str) -> str:
return urllib.parse.quote(code, safe="~()*!.'")
def create_marimo_app_code(
*,
code: str,
app_width: str = "wide",
) -> str:
header = "\n".join(
[
"import marimo",
f'app = marimo.App(width="{app_width}")',
"",
]
) + "\n".join(
[
"",
"@app.cell",
"def __():",
" import marimo as mo",
" return",
]
)
return header + code
def create_marimo_app_url(code: str, mode: str = "read") -> str:
encoded_code = uri_encode_component(code)
return f"https://marimo.app/?code={encoded_code}&embed=true&mode={mode}"
class MarimoBlocksExtension(BlocksExtension):
def extendMarkdownBlocks(self, md: Any, block_mgr: Any) -> None:
block_mgr.register(MarimoEmbedBlock, self.getConfigs())
block_mgr.register(MarimoEmbedFileBlock, self.getConfigs())
def makeExtension(*args: Any, **kwargs: Any) -> MarimoBlocksExtension:
return MarimoBlocksExtension(*args, **kwargs)
```
File: /Users/morganmcguire/ML/marimo/docs/stylesheets/extra.css
```css
/* Adjust the max width of the content */
.md-grid {
max-width: 1440px;
}
/* Custom styling for code blocks */
.highlight {
background-color: var(--md-code-bg-color);
border-radius: 4px;
}
/* Custom styling for admonitions */
.admonition {
border-left-width: 4px !important;
}
/* Custom font sizes for better readability */
.md-typeset {
font-size: 0.75rem;
line-height: 1.6;
}
.md-typeset h1 {
font-size: 2em;
font-weight: 600;
}
/* Custom styling for tables */
.md-typeset table:not([class]) {
font-size: 0.75rem;
}
/* Custom styling for navigation */
.md-nav {
font-size: 0.7rem;
}
/* Custom styling for code annotations */
.md-annotation__index {
font-family: var(--md-code-font-family);
}
.md-header__topic {
visibility: hidden;
}
.md-header__button.md-logo {
margin: 0;
padding: 0;
height: 100%;
}
.md-header__button.md-logo img,
.md-header__button.md-logo svg {
height: 55px;
}
iframe.demo {
width: 100%;
height: 250px;
border: 1px solid var(--md-code-bg-color);
border-radius: 5px;
}
iframe.demo.medium {
height: 400px;
}
iframe.demo.large {
height: 600px;
}
iframe.demo.xlarge {
height: 900px;
}
iframe.demo.xxlarge {
height: 1200px;
}
figure img {
border: 1px solid #ebebeb;
border-radius: 5px;
}
video {
border: 1px solid #ebebeb;
border-radius: 5px;
}
```
File: /Users/morganmcguire/ML/marimo/docs/community.md
```md
# Community
We're building a community. Come hang out with us!
- 🌟 [Star us on GitHub](https://github.com/marimo-team/marimo)
- 💬 [Chat with us on Discord](https://marimo.io/discord?ref=readme)
- 📧 [Subscribe to our Newsletter](https://marimo.io/newsletter)
- ☁️ [Join our Cloud Waitlist](https://marimo.io/cloud)
- ✏️ [Start a GitHub Discussion](https://github.com/marimo-team/marimo/discussions)
- 🦋 [Follow us on Bluesky](https://bsky.app/profile/marimo.io)
- 🐦 [Follow us on Twitter](https://twitter.com/marimo_io)
- 🕴️ [Follow us on LinkedIn](https://www.linkedin.com/company/marimo-io)
## Shields
You can use our shield for opening a marimo application:
<a target="_blank" href="https://marimo.app/l/c7h6pz">
<img src="https://marimo.io/shield.svg"/>
</a>
---
**Markdown**
```markdown
[![marimo](https://marimo.io/shield.svg)](https://marimo.app/l/c7h6pz)
```
**HTML**
```html
<a target="_blank" href="https://marimo.app/l/c7h6pz">
<img src="https://marimo.io/shield.svg" />
</a>
```
```
File: /Users/morganmcguire/ML/marimo/docs/__init__.py
```py
"""marimo documentation package."""
from . import blocks
__all__ = ["blocks"]
```
File: /Users/morganmcguire/ML/marimo/docs/examples.md
```md
# Examples
We have a large [library of
examples](https://github.com/marimo-team/marimo/tree/main/examples) in our
repo. Each example [encapsulates its own Python dependencies in a package
sandbox](guides/editor_features/package_management.md), making it easy to run.
We've deployed some of these examples at our [public
gallery](https://marimo.io/@public).
We spotlight projects from the community each week on [our Twitter](https://x.com/marimo_io); check out our
[spotlights repo](https://github.com/marimo-team/spotlights) for a running
archive, with links to notebooks.
```
File: /Users/morganmcguire/ML/marimo/docs/faq.md
```md
---
hide:
- navigation
---
## Choosing marimo
<a name="faq-jupyter"></a>
### How is marimo different from Jupyter?
marimo is a reinvention of the Python notebook as a reproducible, interactive,
and shareable Python program that can be executed as scripts or deployed as
interactive web apps.
**Consistent state.** In marimo, your notebook code, outputs, and program state
are guaranteed to be consistent. Run a cell and marimo reacts by automatically
running the cells that reference its variables. Delete a cell and marimo scrubs
its variables from program memory, eliminating hidden state.
**Built-in interactivity.** marimo also comes with [UI
elements](guides/interactivity.md) like sliders, a dataframe transformer, and
interactive plots that are automatically synchronized with Python. Interact
with an element and the cells that use it are automatically re-run with its
latest value.
**Pure Python programs.** Unlike Jupyter notebooks, marimo notebooks are stored
as pure Python files that can be executed as scripts, deployed as interactive
web apps, and versioned easily with Git.
<a name="faq-problems"></a>
### What problems does marimo solve?
marimo solves problems in reproducibility, maintainability, interactivity,
reusability, and shareability of notebooks.
**Reproducibility.**
In Jupyter notebooks, the code you see doesn't necessarily match the outputs on
the page or the program state. If you
delete a cell, its variables stay in memory, which other cells may still
reference; users can execute cells in arbitrary order. This leads to
widespread reproducibility issues. [One study](https://blog.jetbrains.com/datalore/2020/12/17/we-downloaded-10-000-000-jupyter-notebooks-from-github-this-is-what-we-learned/#consistency-of-notebooks) analyzed 10 million Jupyter
notebooks and found that 36% of them weren't reproducible.
In contrast, marimo guarantees that your code, outputs, and program state are
consistent, eliminating hidden state and making your notebook reproducible.
marimo achieves this by intelligently analyzing your code and understanding the
relationships between cells, and automatically re-running cells as needed.
In addition, marimo notebooks can serialize package requirements inline;
marimo runs these "sandboxed" notebooks in temporary virtual environments,
making them [reproducible down to the packages](guides/editor_features/package_management.md).
**Maintainability.**
marimo notebooks are stored as pure Python programs (`.py` files). This lets you
version them with Git; in contrast, Jupyter notebooks are stored as JSON and
require extra steps to version.
**Interactivity.**
marimo notebooks come with [UI elements](guides/interactivity.md) that are
automatically synchronized with Python (like sliders, dropdowns); _eg_, scrub a
slider and all cells that reference it are automatically re-run with the new
value. This is difficult to get working in Jupyter notebooks.
**Reusability.**
marimo notebooks can be executed as Python scripts from the command-line (since
they're stored as `.py` files). In contrast, this requires extra steps to
do for Jupyter, such as copying and pasting the code out or using external
frameworks. In the future, we'll also let you import symbols (functions,
classes) defined in a marimo notebook into other Python programs/notebooks,
something you can't easily do with Jupyter.
**Shareability.**
Every marimo notebook can double as an interactive web app, complete with UI
elements, which you can serve using the `marimo run` command. This isn't
possible in Jupyter without substantial extra effort.
_To learn more about problems with traditional notebooks,
see these references
[[1]](https://austinhenley.com/pubs/Chattopadhyay2020CHI_NotebookPainpoints.pdf)
[[2]](https://www.youtube.com/watch?v=7jiPeIFXb6U&t=1s)._
<a name="faq-widgets"></a>
### How is `marimo.ui` different from Jupyter widgets?
Unlike Jupyter widgets, marimo's interactive elements are automatically
synchronized with the Python kernel: no callbacks, no observers, no manually
re-running cells.
<p align="center">
<video autoplay muted loop playsinline width="600px" align="center">
<source src="/_static/faq-marimo-ui.mp4" type="video/mp4">
<source src="/_static/faq-marimo-ui.webm" type="video/webm">
</video>
</p>
## Using marimo
<a name="faq-notebook-or-library"></a>
### Is marimo a notebook or a library?
marimo is both a notebook and a library.
- Create _marimo notebooks_ with the editor that opens in your
browser when you run `marimo edit`.
- Use the _marimo library_ (`import marimo as mo`) in
marimo notebooks. Write markdown with `mo.md(...)`,
create stateful interactive elements with `mo.ui` (`mo.ui.slider(...)`), and
more. See the docs for an [API reference](./api/index.md).
<a name="faq-notebook-app"></a>
### What's the difference between a marimo notebook and a marimo app?
marimo programs are notebooks, apps, or both, depending on how you use them.
There are two ways to interact with a marimo program:
1. open it as a computational _notebook_ with `marimo edit`
2. run it as an interactive _app_ with `marimo run`
All marimo programs start as notebooks, since they are created with `marimo
edit`. Because marimo notebooks are reactive and have built-in interactive
elements, many can easily be made into useful and beautiful apps by simply
hiding the notebook code: this is what `marimo run` does.
Not every notebook needs to be run as an app — marimo notebooks are useful in
and of themselves for rapidly exploring data and doing reproducible science.
And not every app is improved by interacting with the notebook. In some
settings, such as collaborative research, education, and technical
presentations, going back and forth between the notebook view and app view
(which you can do from `marimo edit`) can be useful!
<a name="faq-reactivity"></a>
### How does marimo know what cells to run?
marimo reads each cell once to determine what global names it defines and what
global names it reads. When a cell is run, marimo runs all other cells that
read any of the global names it defines. A global name can refer to a variable,
class, function, or import.
In other words, marimo uses _static analysis_ to make a dataflow graph out of
your cells. Each cell is a node in the graph across which global
variables "flow". Whenever a cell is run, either because you changed its
code or interacted with a UI element it reads, all its descendants run in turn.
<a name="faq-overhead"></a>
### Does marimo slow my code down?
No, marimo doesn't slow your code down. marimo determines the dependencies
among cells by reading your code, not running or tracing it, so there's
zero runtime overhead.
<a name="faq-expensive"></a>
### How do I prevent automatic execution from running expensive cells?
Reactive (automatic) execution ensures your code and outputs are always
in sync, improving reproducibility by eliminating hidden state and
out-of-order execution; marimo also takes care to run only the minimal set of
cells needed to keep your notebook up to date. But when some cells take a long
time to run, it's understandable to be concerned that automatic execution will
kick off expensive cells before you're ready to run them.
_Here are some tips to avoid accidental execution of expensive cells:_
- [Disable expensive cells](guides/reactivity.md#disabling-cells). When a cell
is disabled, it and its descendants are blocked from running.
- Wrap UI elements in a [form][marimo.ui.form].
- Use [`mo.stop`][marimo.stop] to conditionally stop
execution of a cell and its descendants.
- Decorate functions with marimo's [`mo.cache`][marimo.cache] to cache
expensive intermediate computations.
- Use [`mo.persistent_cache`][marimo.persistent_cache] to cache variables to
disk; on re-run, marimo will read values from disk instead of recalculating
them as long as the cell is not stale.
- Disable automatic execution in the [runtime configuration](guides/configuration/runtime_configuration.md).
<a name="faq-lazy"></a>
### How do I disable automatic execution?
You can disable automatic execution through the notebook runtime settings;
see the [guide on runtime configuration](guides/configuration/runtime_configuration.md).
When automatic execution is disabled, marimo still gives you guarantees on
your notebook state and automatically marks cells as stale when appropriate.
<a name="faq-interactivity"></a>
### How do I use sliders and other interactive elements?
Interactive UI elements like sliders are available in `marimo.ui`.
- Assign the UI element to a global variable (`slider = mo.ui.slider(0, 100)`)
- Include it in the last expression of a cell to display it (`slider` or `mo.md(f"Choose a value: {slider}")`)
- Read its current value in another cell via its `value` attribute (`slider.value`)
_When a UI element bound to a global variable is interacted with, all cells
referencing the global variable are run automatically_.
If you have many UI elements or don't know the elements
you'll create until runtime, use `marimo.ui.array` and `marimo.ui.dictionary`
to create UI elements that wrap other UI elements (`sliders =
mo.ui.array([slider(1, 100) for _ in range(n_sliders)])`).
All this and more is explained in the UI tutorial. Run it with
```bash
marimo tutorial ui
```
at the command line.
<a name="faq-form"></a>
### How do I add a submit button to UI elements?
Use the `form` method to add a submit button to a UI element. For
example,
```python
form = marimo.ui.text_area().form()
```
When wrapped in a form, the
text area's value will only be sent to Python when you click the submit button.
Access the last submitted value of the text area with `form.value`.
<a name="faq-markdown"></a>
### How do I write markdown?
Import `marimo` (as `mo`) in a notebook, and use the `mo.md` function.
Learn more in the [outputs guide](guides/outputs.md#markdown)
or by running `marimo tutorial markdown`.
<a name="faq-plots"></a>
### How do I display plots?
Include plots in the last expression of a cell to display them, just like all
other outputs. If you're using matplotlib, you can display the `Figure` object
(get the current figure with `plt.gcf()`). For examples, run the plots tutorial:
```bash
marimo tutorial plots
```
Also see the [plotting API reference](api/plotting.md).
<a name="faq-mpl-cutoff"></a>
### How do I prevent matplotlib plots from being cut off?
If your legend or axes labels are cut off, try calling `plt.tight_layout()`
before outputting your plot:
```python
import matplotlib.pyplot as plt
plt.plot([-8, 8])
plt.ylabel("my variable")
plt.tight_layout()
plt.gca()
```
<a name="faq-interactive-plots"></a>
### How do I display interactive matplotlib plots?
Use [`marimo.mpl.interactive`][marimo.mpl.interactive].
```bash
fig, ax = plt.subplots()
ax.plot([1, 2])
mo.mpl.interactive(ax)
```
<a name="faq-rows-columns"></a>
### How do I display objects in rows and columns?
Use `marimo.hstack` and `marimo.vstack`. See the layout tutorial for details:
```bash
marimo tutorial layout
```
<a name="faq-show-code"></a>
### How do I show cell code in the app view?(#faq-show-code)
Use [`mo.show_code`][marimo.show_code].
<a name="faq-dynamic-ui-elements"></a>
### How do I create an output with a dynamic number of UI elements?
Use [`mo.ui.array`][marimo.ui.array],
[`mo.ui.dictionary`][marimo.ui.dictionary], or
[`mo.ui.batch`][marimo.ui.batch] to create a UI element
that wraps a dynamic number of other UI elements.
If you need custom
formatting, use [`mo.ui.batch`][marimo.ui.batch], otherwise
use [`mo.ui.array`][marimo.ui.array] or
[`mo.ui.dictionary`][marimo.ui.dictionary].
For usage examples, see the
[recipes for grouping UI elements together](recipes.md#grouping-ui-elements-together).
<a name="faq-restart"></a>
### How do I restart a notebook?
To clear all program memory and restart the notebook from scratch, open the
notebook menu in the top right and click "Restart kernel".
<a name="faq-reload"></a>
### How do I reload modules?
Enable automatic reloading of modules via the runtime settings in your
marimo installation's user configuration. (Click the "gear" icon in the
top right of a marimo notebook).
When enabled, marimo will automatically hot-reload modified modules
before executing a cell.
<a name="faq-on-change-called"></a>
### Why aren't my `on_change`/`on_click` handlers being called?
A UI Element's `on_change` (or for buttons, `on_click`) handlers are only
called if the element is bound to a global variable. For example, this won't work
```python
mo.vstack([mo.ui.button(on_change=lambda _: print("I was called")) for _ in range(10)])
```
In such cases (when you want to output a dynamic number of UI elements),
you need to use
[`mo.ui.array`][marimo.ui.array],
[`mo.ui.dictionary`][marimo.ui.dictionary], or
[`mo.ui.batch`][marimo.ui.batch].
See the
[recipes for grouping UI elements together](recipes.md#grouping-ui-elements-together)
for example code.
<a name="faq-on-change-last"></a>
### Why are my `on_change` handlers in an array all referencing the last element?
**Don't do this**: In the below snippet, every `on_change` will print `9`!.
```python
array = mo.ui.array(
[mo.ui.button(on_change=lambda value: print(i)) for i in range(10)
])
```
**Instead, do this**: Explicitly bind `i` to the current loop value:
```python
array = mo.ui.array(
[mo.ui.button(on_change=lambda value, i=i: print(i)) for i in range(10)]
)
array
```
This is necessary because [in Python, closures are late-binding](https://docs.python-guide.org/writing/gotchas/#late-binding-closures).
<a name="faq-sql-brackets"></a>
### Why aren't my SQL brackets working?
Our "SQL" cells are really just Python under the hood to keep notebooks as pure Python scripts. By default, we use `f-strings` for SQL strings, which allows for parameterized SQL like `SELECT * from table where value < {min}`.
To escape real `{` / `}` that you don't want parameterized, use double `\{\{...\}\}`:
```sql
SELECT unnest([\{\{'a': 42, 'b': 84\}\}, \{\{'a': 100, 'b': NULL\}\}]);
```
<a name="faq-annotations"></a>
### How does marimo treat type annotations?
Type annotations are registered as references of a cell, unless they
are explicitly written as strings. This helps ensure correctness of code that
depends on type annotations at runtime (_e.g._, Pydantic), while still
providing a way to omit annotations from affecting dataflow graph.
For example, in
```python
x: A = ...
```
`A` is treated as a reference, used in determining the dataflow graph, but
in
```python
x: "A" = ...
```
`A` isn't made a reference.
For Python 3.12+, marimo additionally implements annotation scoping.
<a name="faq-dotenv"></a>
### How do I use dotenv?
The package `dotenv`'s `loadenv()` function does not work out-of-the box in
marimo. Instead, use `dotenv.load_dotenv(dotenv.find_dotenv(usecwd=True))`.
<a name="faq-packages"></a>
### What packages can I use?
You can use any Python package. marimo cells run arbitrary Python code.
<a name="faq-remote"></a>
### How do I use marimo on a remote server?
Use SSH port-forwarding to run marimo on a remote server
and connect to it from a browser on your local machine. Make sure
to pass the `--headless` flag when starting marimo on remote:
```bash
marimo edit --headless
```
You may also want to set a custom host and port:
```bash
marimo edit --headless --host 0.0.0.0 --port 8080
```
<a name="faq-interfaces"></a>
### How do I make marimo accessible on all network interfaces?
Use `--host 0.0.0.0` with `marimo edit`, `marimo run`, or `marimo tutorial`:
```bash
marimo edit --host 0.0.0.0
```
<a name="faq-jupyter-hub"></a>
### How do I use marimo behind JupyterHub?
JupyterHub can be configured to launch marimo using the [`jupyter-marimo-proxy`
package](https://github.com/jyio/jupyter-marimo-proxy).
<a name="faq-jupyter-book"></a>
### How do I use marimo with JupyterBook?
[JupyterBook](https://jupyterbook.org/en/stable/intro.html) makes it easy
to create static websites with markdown and Jupyter notebooks.
To include a marimo notebook in a JupyterBook, you can either export your
notebook to an `ipynb` file, or export to `HTML`:
1. export to ipynb: `marimo export ipynb my_notebook.py -o my_notebook.ipynb --include-outputs`
2. export to HTML: `marimo export html my_notebook.py -o my_notebook.html`
<a name="faq-app-deploy"></a>
### How do I deploy apps?
Use the marimo CLI's `run` command to serve a notebook as an app:
```bash
marimo run notebook.py
```
If you are running marimo inside a Docker container, you may want to run under a different host and port:
```bash
marimo run notebook.py --host 0.0.0.0 --port 8080
```
<a name="faq-marimo-free"></a>
### Is marimo free?
Yes!
```
File: /Users/morganmcguire/ML/marimo/docs/index.md
```md
---
hide:
- navigation
---
<style>
.md-typeset h1,
.md-content__button {
display: none;
}
</style>
<p align="center" style="margin-top: 40px; margin-bottom: 40px;">
<img src="_static/marimo-logotype-thick.svg" width="210px">
</p>
marimo is a reactive Python notebook: run a cell or interact with a UI
element, and marimo automatically runs dependent cells (or [marks them as
stale](guides/reactivity.md#configuring-how-marimo-runs-cells)), keeping code and outputs
consistent and preventing bugs before they happen. Every marimo notebook is
stored as pure Python, executable as a script, and deployable as an app.
/// admonition | Built from the ground up
type: tip
marimo was built from the ground up to solve <a href="faq.html#faq-jupyter">well-known problems associated with traditional notebooks</a>.
///
/// tab | install with pip
```bash
pip install marimo && marimo tutorial intro
```
///
/// tab | install with uv
```bash
uv pip install marimo && marimo tutorial intro
```
///
/// tab | install with conda
```bash
conda install -c conda-forge marimo && marimo tutorial intro
```
///
Developer experience is core to marimo, with an emphasis on
reproducibility, maintainability, composability, and shareability.
## Highlights
- 🚀 **batteries-included:** replaces `jupyter`, `streamlit`, `jupytext`, `ipywidgets`, `papermill`, and more
- ⚡️ **reactive**: run a cell, and marimo reactively [runs all dependent cells](guides/reactivity.md) or <a href="#expensive-notebooks">marks them as stale</a>
- 🖐️ **interactive:** [bind sliders, tables, plots, and more](guides/interactivity.md) to Python — no callbacks required
- 🔬 **reproducible:** [no hidden state](guides/reactivity.md), deterministic execution, [built-in package management](guides/editor_features/package_management.md)
- 🏃 **executable:** [execute as a Python script](guides/scripts.md), parameterized by CLI args
- 🧪 **testable:** [run your favorite test suite](guides/testing/index.md), verify your notebook's correctness
- 🛜 **shareable**: [deploy as an interactive web app](guides/apps.md) or [slides](guides/apps.md#slides-layout), [run in the browser via WASM](guides/wasm.md)
- 🛢️ **designed for data**: query dataframes and databases [with SQL](guides/working_with_data/sql.md), filter and search [dataframes](guides/working_with_data/dataframes.md)
- 🐍 **git-friendly:** notebooks are stored as `.py` files
- ⌨️ **a modern editor**: [GitHub Copilot](guides/editor_features/ai_completion.md#github-copilot), [AI assistants](guides/editor_features/ai_completion.md#using-ollama), vim keybindings, variable explorer, and [more](guides/editor_features/index.md)
## A reactive programming environment
marimo guarantees your notebook code, outputs, and program state are consistent. This [solves many problems](faq.md#faq-problems) associated with traditional notebooks like Jupyter.
**A reactive programming environment.**
Run a cell and marimo _reacts_ by automatically running the cells that
reference its variables, eliminating the error-prone task of manually
re-running cells. Delete a cell and marimo scrubs its variables from program
memory, eliminating hidden state.
<video autoplay muted loop playsinline width="700px" align="center">
<source src="/_static/reactive.mp4" type="video/mp4">
<source src="/_static/reactive.webm" type="video/webm">
</video>
<a name="expensive-notebooks"></a>
**Compatible with expensive notebooks.** marimo lets you [configure the runtime
to be
lazy](guides/configuration/runtime_configuration.md),
marking affected cells as stale instead of automatically running them. This
gives you guarantees on program state while preventing accidental execution of
expensive cells.
**Synchronized UI elements.** Interact with [UI
elements](guides/interactivity.md) like [sliders](api/inputs/slider.md#slider),
[dropdowns](api/inputs/dropdown.md), [dataframe
transformers](api/inputs/dataframe.md), and [chat
interfaces](api/inputs/chat.md), and the cells that
use them are automatically re-run with their latest values.
<video autoplay muted loop playsinline width="700px" align="center">
<source src="/_static/readme-ui.mp4" type="video/mp4">
<source src="/_static/readme-ui.webm" type="video/webm">
</video>
**Interactive dataframes.** [Page through, search, filter, and
sort](./guides/working_with_data/dataframes.md)
millions of rows blazingly fast, no code required.
<video autoplay muted loop playsinline width="100%" height="100%" align="center">
<source src="/_static/docs-df.mp4" type="video/mp4">
<source src="/_static/docs-df.webm" type="video/webm">
</video>
**Performant runtime.** marimo runs only those cells that need to be run by
statically analyzing your code.
**Dynamic markdown and SQL.** Use markdown to tell dynamic stories that depend on
Python data. Or build [SQL](guides/working_with_data/sql.md) queries
that depend on Python values and execute them against dataframes, databases,
CSVs, Google Sheets, or anything else using our built-in SQL engine, which
returns the result as a Python dataframe.
<img src="https://raw.githubusercontent.com/marimo-team/marimo/main/docs/_static/readme-sql-cell.png" width="700px" />
Your notebooks are still pure Python, even if they use markdown or SQL.
**Deterministic execution order.** Notebooks are executed in a deterministic
order, based on variable references instead of cells' positions on the page.
Organize your notebooks to best fit the stories you'd like to tell.
**Built-in package management.** marimo has built-in support for all major
package managers, letting you [install packages on import](guides/editor_features/package_management.md). marimo can even
[serialize package
requirements](guides/package_reproducibility.md)
in notebook files, and auto install them in
isolated venv sandboxes.
**Batteries-included.** marimo comes with GitHub Copilot, AI assistants, Ruff
code formatting, HTML export, fast code completion, a [VS Code
extension](https://marketplace.visualstudio.com/items?itemName=marimo-team.vscode-marimo),
an interactive dataframe viewer, and [many more](guides/editor_features/index.md)
quality-of-life features.
## Quickstart
**Installation.** In a terminal, run
```bash
pip install marimo # or conda install -c conda-forge marimo
marimo tutorial intro
```
**Create notebooks.**
Create or edit notebooks with
```bash
marimo edit
```
**Run apps.** Run your notebook as a web app, with Python
code hidden and uneditable:
```bash
marimo run your_notebook.py
```
<video autoplay muted loop playsinline width="450px" align="center" style="border-radius: 8px">
<source src="/_static/docs-model-comparison.mp4" type="video/mp4">
<source src="/_static/docs-model-comparison.webm" type="video/webm">
</video>
**Execute as scripts.** Execute a notebook as a script at the
command line:
```bash
python your_notebook.py
```
**Automatically convert Jupyter notebooks.** Automatically convert Jupyter
notebooks to marimo notebooks with the CLI
```bash
marimo convert your_notebook.ipynb > your_notebook.py
```
or use our [web interface](https://marimo.io/convert).
**Tutorials.**
List all tutorials:
```bash
marimo tutorial --help
```
## Questions?
See our [FAQ](faq.md).
## Learn more
marimo is easy to get started with, with lots of room for power users.
For example, here's an embedding visualizer made in marimo
([video](https://marimo.io/videos/landing/full.mp4)):
<video autoplay muted loop playsinline width="700px" align="center">
<source src="/_static/embedding.mp4" type="video/mp4">
<source src="/_static/embedding.webm" type="video/webm">
</video>
Check out our [guides](guides/index.md), our [example
gallery](https://marimo.io/gallery), and our
[`examples/`](https://github.com/marimo-team/marimo/tree/main/examples) on
GitHub to learn more.
<table border="0">
<tr>
<td>
<a target="_blank" href="getting_started/key_concepts">
<video autoplay muted loop playsinline style="max-height: 150px; width: auto; display: block">
<source src="/_static/reactive.mp4" type="video/mp4">
<source src="/_static/reactive.webm" type="video/webm">
</video>
</a>
</td>
<td>
<a target="_blank" href="api/inputs/">
<video autoplay muted loop playsinline style="max-height: 150px; width: auto; display: block">
<source src="/_static/readme-ui.mp4" type="video/mp4">
<source src="/_static/readme-ui.webm" type="video/webm">
</video>
</a>
</td>
<td>
<a target="_blank" href="guides/working_with_data/plotting">
<video autoplay muted loop playsinline style="max-height: 150px; width: auto; display: block">
<source src="/_static/docs-intro.mp4" type="video/mp4">
<source src="/_static/docs-intro.webm" type="video/webm">
</video>
</a>
</td>
<td>
<a target="_blank" href="api/layouts/">
<video autoplay muted loop playsinline style="max-height: 150px; width: auto; display: block">
<source src="/_static/outputs.mp4" type="video/mp4">
<source src="/_static/outputs.webm" type="video/webm">
</video>
</a>
</td>
</tr>
<tr>
<td>
<a target="_blank" href="getting_started/key_concepts"> Tutorial </a>
</td>
<td>
<a target="_blank" href="api/inputs/"> Inputs </a>
</td>
<td>
<a target="_blank" href="guides/working_with_data/plotting"> Plots </a>
</td>
<td>
<a target="_blank" href="api/layouts/"> Layout </a>
</td>
</tr>
<tr>
<td>
<a target="_blank" href="https://marimo.app/l/c7h6pz">
<img src="https://marimo.io/shield.svg"/>
</a>
</td>
<td>
<a target="_blank" href="https://marimo.app/l/0ue871">
<img src="https://marimo.io/shield.svg"/>
</a>
</td>
<td>
<a target="_blank" href="https://marimo.app/l/lxp1jk">
<img src="https://marimo.io/shield.svg"/>
</a>
</td>
<td>
<a target="_blank" href="https://marimo.app/l/14ovyr">
<img src="https://marimo.io/shield.svg"/>
</a>
</td>
</tr>
</table>
## Contributing
We appreciate all contributions! You don't need to be an expert to help out.
Please see [CONTRIBUTING.md](https://github.com/marimo-team/marimo/blob/main/CONTRIBUTING.md) for more details on how to get
started.
> Questions? Reach out to us [on Discord](https://marimo.io/discord?ref=docs).
## Community
We're building a community. Come hang out with us!
- 🌟 [Star us on GitHub](https://github.com/marimo-team/marimo)
- 💬 [Chat with us on Discord](https://marimo.io/discord?ref=docs)
- 📧 [Subscribe to our Newsletter](https://marimo.io/newsletter)
- ☁️ [Join our Cloud Waitlist](https://marimo.io/cloud)
- ✏️ [Start a GitHub Discussion](https://github.com/marimo-team/marimo/discussions)
- 💬 [Follow us on Bluesky](https://bsky.app/profile/marimo.io)
- 🐦 [Follow us on Twitter](https://twitter.com/marimo_io)
- 💬 [Follow us on Mastodon](https://mastodon.social/@marimo_io)
- 🕴️ [Follow us on LinkedIn](https://www.linkedin.com/company/marimo-io)
## Inspiration ✨
marimo is a **reinvention** of the Python notebook as a reproducible, interactive,
and shareable Python program, instead of an error-prone JSON scratchpad.
We believe that the tools we use shape the way we think — better tools, for
better minds. With marimo, we hope to provide the Python community with a
better programming environment to do research and communicate it; to experiment
with code and share it; to learn computational science and teach it.
Our inspiration comes from many places and projects, especially
[Pluto.jl](https://github.com/fonsp/Pluto.jl),
[ObservableHQ](https://observablehq.com/tutorials), and
[Bret Victor's essays](http://worrydream.com/). marimo is part of
a greater movement toward reactive dataflow programming. From
[IPyflow](https://github.com/ipyflow/ipyflow), [streamlit](https://github.com/streamlit/streamlit),
[TensorFlow](https://github.com/tensorflow/tensorflow),
[PyTorch](https://github.com/pytorch/pytorch/tree/main),
[JAX](https://github.com/google/jax), and
[React](https://github.com/facebook/react), the ideas of functional,
declarative, and reactive programming are transforming a broad range of tools
for the better.
<p align="right">
<img src="https://raw.githubusercontent.com/marimo-team/marimo/main/docs/_static/marimo-logotype-horizontal.png" height="200px">
</p>
```
File: /Users/morganmcguire/ML/marimo/docs/recipes.md
```md
# Recipes
This page includes code snippets or "**recipes**" for a variety of common tasks.
Use them as building blocks or examples when making your own notebooks.
In these recipes, **each code block represents a cell**.
## Control Flow
### Show an output conditionally
**Use cases.** Hide an output until a condition is met (_e.g._, until algorithm
parameters are valid), or show different outputs depending on the value of a UI
element or some other Python object
**Recipe.**
1. Use an `if` expression to choose which output to show.
```python
# condition is a boolean, True of False
condition = True
"condition is True" if condition else None
```
### Run a cell on a timer
**Use cases.**
- Load new data periodically, and show updated plots or other outputs. For
example, in a dashboard monitoring a training run, experiment trial,
real-time weather data, ...
- Run a job periodically
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a [`mo.ui.refresh`][marimo.ui.refresh] timer that fires once a second:
```python
refresh = mo.ui.refresh(default_interval="1s")
# This outputs a timer that fires once a second
refresh
```
3. Reference the timer by name to make this cell run once a second
```python
import random
# This cell will run once a second!
refresh
mo.md("#" + "🍃" * random.randint(1, 10))
```
!!! note "Requires 'on cell change' autorun"
For this to work, the [runtime
configuration's](guides/configuration/runtime_configuration.md) `on cell
change` should be set to `autorun`
### Require form submission before sending UI value
**Use cases.** UI elements automatically send their values to the Python when
they are interacted with, and run all cells referencing the elements. This
makes marimo notebooks responsive, but it can be an issue when the
downstream cells are expensive, or when the input (such as a text box)
needs to be filled out completely before it is considered valid. Forms
let you gate submission of UI element values on manual confirmation, via a
button press.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a submittable form.
```python
form = mo.ui.text(label="Your name").form()
form
```
3. Get the value of the form.
```python
form.value
```
### Stop execution of a cell and its descendants
**Use cases.** For example, don't run a cell or its descendants if a form is
unsubmitted.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a submittable form.
```python
form = mo.ui.text(label="Your name").form()
form
```
3. Use [`mo.stop`][marimo.stop] to stop execution when
the form is unsubmitted.
```python
mo.stop(form.value is None, mo.md("Submit the form to continue"))
mo.md(f"Hello, {form.value}!")
```
## Grouping UI elements together
### Create an array of UI elements
**Use cases.** In order to synchronize UI elements between the frontend and
backend (Python), marimo requires you to
[assign UI elements to global variables](guides/interactivity.md).
But sometimes you don't know the number of elements to make until runtime:
for example, maybe you want to make a list of sliders, and the number of sliders
to make depends on the value of some other UI element.
You might be tempted to create a Python list of UI elements,
such as `l = [mo.ui.slider(1, 10) for i in range(number.value)]`: _however,
this won't work, because the sliders are not bound to global variables_.
For such cases, marimo provides the "higher-order" UI element
[`mo.ui.array`][marimo.ui.array], which lets you make
a new UI element out of a list of UI elements:
`l = mo.ui.array([mo.ui.slider(1, 10) for i in range(number.value)])`.
The value of an `array` element is a list of the values of the elements
it wraps (in this case, a list of the slider values). Any time you interact
with any of the UI elements in the array, all cells referencing the array
by name (in this case, "`l`") will run automatically.
**Recipe.**
1. Import packages.
```python
import marimo as mo
```
2. Use [`mo.ui.array`][marimo.ui.array] to group together
many UI elements into a list.
```python
import random
# instead of random.randint, in your notebook you'd use the value of
# an upstream UI element or other Python object
array = mo.ui.array([mo.ui.text() for i in range(random.randint(1, 10))])
array
```
3. Get the value of the UI elements using `array.value`
```python
array.value
```
### Create a dictionary of UI elements
**Use cases.** Same as for creating an array of UI elements, but lets you
name each of the wrapped elements with a string key.
**Recipe.**
1. Import packages.
```python
import marimo as mo
```
2. Use [`mo.ui.dictionary`][marimo.ui.dictionary] to
group together many UI elements into a list.
```python
import random
# instead of random.randint, in your notebook you'd use the value of
# an upstream UI element or other Python object
dictionary = mo.ui.dictionary({str(i): mo.ui.text() for i in range(random.randint(1, 10))})
dictionary
```
3. Get the value of the UI elements using `dictionary.value`
```python
dictionary.value
```
### Embed a dynamic number of UI elements in another output
**Use cases.** When you want to embed a dynamic number of UI elements
in other outputs (like tables or markdown).
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Group the elements with
[`mo.ui.dictionary`][marimo.ui.dictionary] or
[`mo.ui.array`][marimo.ui.array], then retrieve them from the container
and display them elsewhere.
```python
import random
n_items = random.randint(2, 5)
# Create a dynamic number of elements using `mo.ui.dictionary` and
# `mo.ui.array`
elements = mo.ui.dictionary(
{
"checkboxes": mo.ui.array([mo.ui.checkbox() for _ in range(n_items)]),
"texts": mo.ui.array(
[mo.ui.text(placeholder="task ...") for _ in range(n_items)]
),
}
)
mo.md(
f"""
Here's a TODO list of {n_items} items\n\n
"""
+ "\n\n".join(
# Iterate over the elements and embed them in markdown
[
f"{checkbox} {text}"
for checkbox, text in zip(
elements["checkboxes"], elements["texts"]
)
]
)
)
```
3. Get the value of the elements
```python
elements.value
```
### Create a hstack (or vstack) of UI elements with `on_change` handlers
**Use cases.** Arrange a dynamic number of UI elements in a hstack or vstack,
for example some number of buttons, and execute some side-effect when an
element is interacted with, e.g. when a button is clicked.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create buttons in `mo.ui.array` and pass them to hstack -- a regular
Python list won't work. Make sure to assign the array to a global variable.
```python
import random
# Create a state object that will store the index of the
# clicked button
get_state, set_state = mo.state(None)
# Create an mo.ui.array of buttons - a regular Python list won't work.
buttons = mo.ui.array(
[
mo.ui.button(
label="button " + str(i), on_change=lambda v, i=i: set_state(i)
)
for i in range(random.randint(2, 5))
]
)
mo.hstack(buttons)
```
3. Get the state value
```python
get_state()
```
### Create a table column of buttons with `on_change` handlers
**Use cases.** Arrange a dynamic number of UI elements in a column of
a table, and execute some side-effect when an element is interacted with, e.g.
when a button is clicked.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create buttons in `mo.ui.array` and pass them to `mo.ui.table`.
Make sure to assign the table and array to global variables
```python
import random
# Create a state object that will store the index of the
# clicked button
get_state, set_state = mo.state(None)
# Create an mo.ui.array of buttons - a regular Python list won't work.
buttons = mo.ui.array(
[
mo.ui.button(
label="button " + str(i), on_change=lambda v, i=i: set_state(i)
)
for i in range(random.randint(2, 5))
]
)
# Put the buttons array into the table
table = mo.ui.table(
{
"Action": ["Action Name"] * len(buttons),
"Trigger": list(buttons),
}
)
table
```
3. Get the state value
```python
get_state()
```
### Create a form with multiple UI elements
**Use cases.** Combine multiple UI elements into a form so that submission
of the form sends all its elements to Python.
**Recipe.**
1. Import packages.
```python
import marimo as mo
```
2. Use [`mo.ui.form`][marimo.ui.form] and [`Html.batch`][marimo.Html.batch] to
create a form with multiple elements.
```python
form = mo.md(
r"""
Choose your algorithm parameters:
- $\epsilon$: {epsilon}
- $\delta$: {delta}
"""
).batch(epsilon=mo.ui.slider(0.1, 1, step=0.1), delta=mo.ui.number(1, 10)).form()
form
```
3. Get the submitted form value.
```python
form.value
```
### Populating form with pre-defined examples
**Use cases.** To give examples of how a filled form looks like. Useful for illustrating complex API requests or database queries.
The form can also be populated from [URL query parameters](./api/query_params.md) (<a href="https://marimo.app/l/w21a3x?t1=query&t2=params">notebook example</a>).
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create dropdown of examples
```python
examples = mo.ui.dropdown(
options={
"ex 1": {"t1": "hello", "t2": "world"},
"ex 2": {"t1": "marimo", "t2": "notebook"},
},
value="ex 1",
label="examples",
)
```
3. Create form from examples.
```
form = (
mo.md(
"""
### Your form
{t1}
{t2}
"""
)
.batch(
t1=mo.ui.text(label="enter text", value=examples.value.get("t1", "")),
t2=mo.ui.text(label="more text", value=examples.value.get("t2", "")),
)
.form(
submit_button_label="go"
)
)
```
4. Run pre-populated from or recompute with new input.
```python
output = (
" ".join(form.value.values()).upper()
if form.value is not None
else " ".join(examples.value.values()).upper()
)
examples, form, output
```
## Working with buttons
### Create a button that triggers computation when clicked
**Use cases.** To trigger a computation on button click and only on button
click, use [`mo.ui.run_button()`](api/inputs/run_button.md).
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a run button
```python
button = mo.ui.run_button()
button
```
3. Run something only if the button has been clicked.
```python
mo.stop(not button.value, "Click 'run' to generate a random number")
import random
random.randint(0, 1000)
```
### Create a counter button
**Use cases.** A counter button, i.e. a button that counts the number of times
it has been clicked, is a helpful building block for reacting to button clicks
(see other recipes in this section).
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Use [`mo.ui.button`][marimo.ui.button] and its
`on_click` argument to create a counter button.
```python
# Initialize the button value to 0, increment it on every click
button = mo.ui.button(value=0, on_click=lambda count: count + 1)
button
```
3. Get the button value
```python
button.value
```
### Create a toggle button
**Use cases.** Toggle between two states using a button with a button
that toggles between `True` and `False`. (Tip: you can also just use
[`mo.ui.switch`][marimo.ui.switch].)
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Use [`mo.ui.button`][marimo.ui.button] and its
`on_click` argument to create a toggle button.
```python
# Initialize the button value to False, flip its value on every click.
button = mo.ui.button(value=False, on_click=lambda value: not value)
button
```
3. Toggle between two outputs using the button value.
```python
mo.md("True!") if button.value else mo.md("False!")
```
### Re-run a cell when a button is pressed
**Use cases.** For example, you have a cell showing a random sample of data,
and you want to resample on button press.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a button without a value, to function as a _trigger_.
```python
button = mo.ui.button()
button
```
3. Reference the button in another cell.
```python
# the button acts as a trigger: every time it is clicked, this cell is run
button
# Replace with your custom logic
import random
random.randint(0, 100)
```
### Run a cell when a button is pressed, but not before
**Use cases.** Wait for confirmation before executing downstream cells
(similar to a form).
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a counter button.
```python
button = mo.ui.button(value=0, on_click=lambda count: count + 1)
button
```
3. Only execute when the count is greater than 0.
```python
# Don't run this cell if the button hasn't been clicked, using mo.stop.
# Alternatively, use an if expression.
mo.stop(button.value == 0)
mo.md(f"The button was clicked {button.value} times")
```
### Reveal an output when a button is pressed
**Use cases.** Incrementally reveal a user interface.
**Recipe.**
1. Import packages
```python
import marimo as mo
```
2. Create a counter button.
```python
button = mo.ui.button(value=0, on_click=lambda count: count + 1)
button
```
3. Show an output after the button is clicked.
```python
mo.md("#" + "🍃" * button.value) if button.value > 0 else None
```
## Caching
### Cache function outputs in memory
**Use case.** Because marimo runs cells automatically as code and
UI elements change, it can be helpful to cache expensive intermediate
computations. For example, perhaps your notebook computes t-SNE, UMAP, or PyMDE
embeddings, and exposes their parameters as UI elements. Caching the embeddings
for different configurations of the elements would greatly speed up your notebook.
**Recipe.**
1. Use [`mo.cache`][marimo.cache] to cache function outputs given inputs.
```python
import marimo as mo
@mo.cache
def compute_predictions(problem_parameters):
# replace with your own function/parameters
...
```
Whenever `compute_predictions` is called with a value of `problem_parameters`
it has not seen, it will compute the predictions and store them in a cache. The
next time it is called with the same parameters, instead of recomputing the
predictions, it will return the previously computed value from the cache.
### Persistent caching for very expensive computations
**Use case.** If you are using marimo to capture very compute intensive
results, you may want to save the state of your computations to disk. Ideally,
if you update your code, then this save should be invalidated. It may also be
advantageous to add UI elements to explore your results, without having to
recompute expensive computations. You can achieve this with
[`mo.persistent_cache`][marimo.persistent_cache].
**Recipe.**
1. Use `mo.persistent_cache` to cache blocks of code to disk.
```python
import marimo as mo
with mo.persistent_cache("my_cache"):
# This block of code, and results will be cached to disk
...
```
If the execution conditions are the same, then cache will load results from
disk, and populate variable definitions.
```
File: /Users/morganmcguire/ML/marimo/docs/hooks.py
```py
import re
from pathlib import Path
from typing import Any
def on_page_markdown(markdown: str, **kwargs: Any) -> str:
del kwargs
static_dir = Path("docs/")
if not static_dir.exists():
return markdown
# Find all src="/" patterns
pattern = r'src="(/[^"]+)"'
matches = re.finditer(pattern, markdown)
for match in matches:
src_path = match.group(1)
# Remove leading slash and check if file exists in _static
relative_path = src_path.lstrip("/")
full_path = static_dir / relative_path
if not full_path.exists():
print(f"⚠️ Warning: Static asset not found: {src_path}")
print(f" Expected at: {full_path}")
return markdown
```
File: /Users/morganmcguire/ML/marimo/docs/vercel.json
```json
{
"redirects": [
{
"source": "/guides/sql.html",
"destination": "/guides/working_with_data/sql.html",
"permanent": true
},
{
"source": "/guides/plotting.html",
"destination": "/guides/working_with_data/plotting.html",
"permanent": true
},
{
"source": "/guides/dataframes.html",
"destination": "/guides/working_with_data/dataframes.html",
"permanent": true
},
{
"source": "/guides/theming.html",
"destination": "/guides/configuration/theming.html",
"permanent": true
},
{
"source": "/guides/overview.html",
"destination": "/getting_started/key_concepts.html",
"permanent": true
},
{
"source": "/api/app.html",
"destination": "/api/app/",
"permanent": true
},
{
"source": "/api/caching.html",
"destination": "/api/caching/",
"permanent": true
},
{
"source": "/api/cell.html",
"destination": "/api/cell/",
"permanent": true
},
{
"source": "/api/cli_args.html",
"destination": "/api/cli_args/",
"permanent": true
},
{
"source": "/api/control_flow.html",
"destination": "/api/control_flow/",
"permanent": true
},
{
"source": "/api/diagrams.html",
"destination": "/api/diagrams/",
"permanent": true
},
{
"source": "/api/html.html",
"destination": "/api/html/",
"permanent": true
},
{
"source": "/api/index.html",
"destination": "/api/",
"permanent": true
},
{
"source": "/api/inputs/anywidget.html",
"destination": "/api/inputs/anywidget/",
"permanent": true
},
{
"source": "/api/inputs/array.html",
"destination": "/api/inputs/array/",
"permanent": true
},
{
"source": "/api/inputs/batch.html",
"destination": "/api/inputs/batch/",
"permanent": true
},
{
"source": "/api/inputs/button.html",
"destination": "/api/inputs/button/",
"permanent": true
},
{
"source": "/api/inputs/chat.html",
"destination": "/api/inputs/chat/",
"permanent": true
},
{
"source": "/api/inputs/checkbox.html",
"destination": "/api/inputs/checkbox/",
"permanent": true
},
{
"source": "/api/inputs/code_editor.html",
"destination": "/api/inputs/code_editor/",
"permanent": true
},
{
"source": "/api/inputs/data_explorer.html",
"destination": "/api/inputs/data_explorer/",
"permanent": true
},
{
"source": "/api/inputs/dataframe.html",
"destination": "/api/inputs/dataframe/",
"permanent": true
},
{
"source": "/api/inputs/dates.html",
"destination": "/api/inputs/dates/",
"permanent": true
},
{
"source": "/api/inputs/dictionary.html",
"destination": "/api/inputs/dictionary/",
"permanent": true
},
{
"source": "/api/inputs/dropdown.html",
"destination": "/api/inputs/dropdown/",
"permanent": true
},
{
"source": "/api/inputs/file.html",
"destination": "/api/inputs/file/",
"permanent": true
},
{
"source": "/api/inputs/file_browser.html",
"destination": "/api/inputs/file_browser/",
"permanent": true
},
{
"source": "/api/inputs/form.html",
"destination": "/api/inputs/form/",
"permanent": true
},
{
"source": "/api/inputs/index.html",
"destination": "/api/inputs/",
"permanent": true
},
{
"source": "/api/inputs/microphone.html",
"destination": "/api/inputs/microphone/",
"permanent": true
},
{
"source": "/api/inputs/multiselect.html",
"destination": "/api/inputs/multiselect/",
"permanent": true
},
{
"source": "/api/inputs/nav_menu.html",
"destination": "/api/inputs/nav_menu/",
"permanent": true
},
{
"source": "/api/inputs/number.html",
"destination": "/api/inputs/number/",
"permanent": true
},
{
"source": "/api/inputs/radio.html",
"destination": "/api/inputs/radio/",
"permanent": true
},
{
"source": "/api/inputs/range_slider.html",
"destination": "/api/inputs/range_slider/",
"permanent": true
},
{
"source": "/api/inputs/refresh.html",
"destination": "/api/inputs/refresh/",
"permanent": true
},
{
"source": "/api/inputs/run_button.html",
"destination": "/api/inputs/run_button/",
"permanent": true
},
{
"source": "/api/inputs/slider.html",
"destination": "/api/inputs/slider/",
"permanent": true
},
{
"source": "/api/inputs/switch.html",
"destination": "/api/inputs/switch/",
"permanent": true
},
{
"source": "/api/inputs/table.html",
"destination": "/api/inputs/table/",
"permanent": true
},
{
"source": "/api/inputs/tabs.html",
"destination": "/api/inputs/tabs/",
"permanent": true
},
{
"source": "/api/inputs/text.html",
"destination": "/api/inputs/text/",
"permanent": true
},
{
"source": "/api/inputs/text_area.html",
"destination": "/api/inputs/text_area/",
"permanent": true
},
{
"source": "/api/layouts/accordion.html",
"destination": "/api/layouts/accordion/",
"permanent": true
},
{
"source": "/api/layouts/callout.html",
"destination": "/api/layouts/callout/",
"permanent": true
},
{
"source": "/api/layouts/carousel.html",
"destination": "/api/layouts/carousel/",
"permanent": true
},
{
"source": "/api/layouts/index.html",
"destination": "/api/layouts/",
"permanent": true
},
{
"source": "/api/layouts/justify.html",
"destination": "/api/layouts/justify/",
"permanent": true
},
{
"source": "/api/layouts/lazy.html",
"destination": "/api/layouts/lazy/",
"permanent": true
},
{
"source": "/api/layouts/plain.html",
"destination": "/api/layouts/plain/",
"permanent": true
},
{
"source": "/api/layouts/routes.html",
"destination": "/api/layouts/routes/",
"permanent": true
},
{
"source": "/api/layouts/sidebar.html",
"destination": "/api/layouts/sidebar/",
"permanent": true
},
{
"source": "/api/layouts/stacks.html",
"destination": "/api/layouts/stacks/",
"permanent": true
},
{
"source": "/api/layouts/tree.html",
"destination": "/api/layouts/tree/",
"permanent": true
},
{
"source": "/api/markdown.html",
"destination": "/api/markdown/",
"permanent": true
},
{
"source": "/api/media/audio.html",
"destination": "/api/media/audio/",
"permanent": true
},
{
"source": "/api/media/download.html",
"destination": "/api/media/download/",
"permanent": true
},
{
"source": "/api/media/image.html",
"destination": "/api/media/image/",
"permanent": true
},
{
"source": "/api/media/index.html",
"destination": "/api/media/",
"permanent": true
},
{
"source": "/api/media/pdf.html",
"destination": "/api/media/pdf/",
"permanent": true
},
{
"source": "/api/media/plain_text.html",
"destination": "/api/media/plain_text/",
"permanent": true
},
{
"source": "/api/media/video.html",
"destination": "/api/media/video/",
"permanent": true
},
{
"source": "/api/miscellaneous.html",
"destination": "/api/miscellaneous/",
"permanent": true
},
{
"source": "/api/outputs.html",
"destination": "/api/outputs/",
"permanent": true
},
{
"source": "/api/plotting.html",
"destination": "/api/plotting/",
"permanent": true
},
{
"source": "/api/query_params.html",
"destination": "/api/query_params/",
"permanent": true
},
{
"source": "/api/state.html",
"destination": "/api/state/",
"permanent": true
},
{
"source": "/api/status.html",
"destination": "/api/status/",
"permanent": true
},
{
"source": "/apps/README.html",
"destination": "/apps/",
"permanent": true
},
{
"source": "/community.html",
"destination": "/community/",
"permanent": true
},
{
"source": "/examples.html",
"destination": "/examples/",
"permanent": true
},
{
"source": "/faq.html",
"destination": "/faq/",
"permanent": true
},
{
"source": "/getting_started/index.html",
"destination": "/getting_started/",
"permanent": true
},
{
"source": "/getting_started/installation.html",
"destination": "/getting_started/installation/",
"permanent": true
},
{
"source": "/getting_started/key_concepts.html",
"destination": "/getting_started/key_concepts/",
"permanent": true
},
{
"source": "/getting_started/quickstart.html",
"destination": "/getting_started/quickstart/",
"permanent": true
},
{
"source": "/guides/apps.html",
"destination": "/guides/apps/",
"permanent": true
},
{
"source": "/guides/best_practices.html",
"destination": "/guides/best_practices/",
"permanent": true
},
{
"source": "/guides/coming_from/index.html",
"destination": "/guides/coming_from/",
"permanent": true
},
{
"source": "/guides/coming_from/jupyter.html",
"destination": "/guides/coming_from/jupyter/",
"permanent": true
},
{
"source": "/guides/coming_from/jupytext.html",
"destination": "/guides/coming_from/jupytext/",
"permanent": true
},
{
"source": "/guides/coming_from/papermill.html",
"destination": "/guides/coming_from/papermill/",
"permanent": true
},
{
"source": "/guides/coming_from/streamlit.html",
"destination": "/guides/coming_from/streamlit/",
"permanent": true
},
{
"source": "/guides/configuration/html_head.html",
"destination": "/guides/configuration/html_head/",
"permanent": true
},
{
"source": "/guides/configuration/index.html",
"destination": "/guides/configuration/",
"permanent": true
},
{
"source": "/guides/configuration/runtime_configuration.html",
"destination": "/guides/configuration/runtime_configuration/",
"permanent": true
},
{
"source": "/guides/configuration/theming.html",
"destination": "/guides/configuration/theming/",
"permanent": true
},
{
"source": "/guides/deploying/authentication.html",
"destination": "/guides/deploying/authentication/",
"permanent": true
},
{
"source": "/guides/deploying/deploying_docker.html",
"destination": "/guides/deploying/deploying_docker/",
"permanent": true
},
{
"source": "/guides/deploying/deploying_hugging_face.html",
"destination": "/guides/deploying/deploying_hugging_face/",
"permanent": true
},
{
"source": "/guides/deploying/deploying_ploomber.html",
"destination": "/guides/deploying/deploying_ploomber/",
"permanent": true
},
{
"source": "/guides/deploying/deploying_public_gallery.html",
"destination": "/guides/deploying/deploying_public_gallery/",
"permanent": true
},
{
"source": "/guides/deploying/deploying_railway.html",
"destination": "/guides/deploying/deploying_railway/",
"permanent": true
},
{
"source": "/guides/deploying/index.html",
"destination": "/guides/deploying/",
"permanent": true
},
{
"source": "/guides/deploying/prebuilt_containers.html",
"destination": "/guides/deploying/prebuilt_containers/",
"permanent": true
},
{
"source": "/guides/deploying/programmatically.html",
"destination": "/guides/deploying/programmatically/",
"permanent": true
},
{
"source": "/guides/editor_features/ai_completion.html",
"destination": "/guides/editor_features/ai_completion/",
"permanent": true
},
{
"source": "/guides/editor_features/hotkeys.html",
"destination": "/guides/editor_features/hotkeys/",
"permanent": true
},
{
"source": "/guides/editor_features/index.html",
"destination": "/guides/editor_features/",
"permanent": true
},
{
"source": "/guides/editor_features/overview.html",
"destination": "/guides/editor_features/overview/",
"permanent": true
},
{
"source": "/guides/editor_features/package_management.html",
"destination": "/guides/editor_features/package_management/",
"permanent": true
},
{
"source": "/guides/expensive_notebooks.html",
"destination": "/guides/expensive_notebooks/",
"permanent": true
},
{
"source": "/guides/exporting.html",
"destination": "/guides/exporting/",
"permanent": true
},
{
"source": "/guides/index.html",
"destination": "/guides/",
"permanent": true
},
{
"source": "/guides/integrating_with_marimo/custom_ui_plugins.html",
"destination": "/guides/integrating_with_marimo/custom_ui_plugins/",
"permanent": true
},
{
"source": "/guides/integrating_with_marimo/displaying_objects.html",
"destination": "/guides/integrating_with_marimo/displaying_objects/",
"permanent": true
},
{
"source": "/guides/integrating_with_marimo/index.html",
"destination": "/guides/integrating_with_marimo/",
"permanent": true
},
{
"source": "/guides/interactivity.html",
"destination": "/guides/interactivity/",
"permanent": true
},
{
"source": "/guides/island_example.html",
"destination": "/guides/island_example/",
"permanent": true
},
{
"source": "/guides/outputs.html",
"destination": "/guides/outputs/",
"permanent": true
},
{
"source": "/guides/reactivity.html",
"destination": "/guides/reactivity/",
"permanent": true
},
{
"source": "/guides/scripts.html",
"destination": "/guides/scripts/",
"permanent": true
},
{
"source": "/guides/state.html",
"destination": "/guides/state/",
"permanent": true
},
{
"source": "/guides/troubleshooting.html",
"destination": "/guides/troubleshooting/",
"permanent": true
},
{
"source": "/guides/wasm.html",
"destination": "/guides/wasm/",
"permanent": true
},
{
"source": "/guides/working_with_data/dataframes.html",
"destination": "/guides/working_with_data/dataframes/",
"permanent": true
},
{
"source": "/guides/working_with_data/index.html",
"destination": "/guides/working_with_data/",
"permanent": true
},
{
"source": "/guides/working_with_data/plotting.html",
"destination": "/guides/working_with_data/plotting/",
"permanent": true
},
{
"source": "/guides/working_with_data/sql.html",
"destination": "/guides/working_with_data/sql/",
"permanent": true
},
{
"source": "/index.html",
"destination": "/",
"permanent": true
},
{
"source": "/integrations/google_cloud_bigquery.html",
"destination": "/integrations/google_cloud_bigquery/",
"permanent": true
},
{
"source": "/integrations/google_cloud_storage.html",
"destination": "/integrations/google_cloud_storage/",
"permanent": true
},
{
"source": "/integrations/google_sheets.html",
"destination": "/integrations/google_sheets/",
"permanent": true
},
{
"source": "/integrations/index.html",
"destination": "/integrations/",
"permanent": true
},
{
"source": "/integrations/motherduck.html",
"destination": "/integrations/motherduck/",
"permanent": true
},
{
"source": "/recipes.html",
"destination": "/recipes/",
"permanent": true
},
{
"source": "/:path*.html",
"destination": "/:path*/",
"permanent": true
},
{
"source": "/:path([^.]*[^/]$)",
"destination": "/:path/",
"permanent": true
}
]
}
```
File: /Users/morganmcguire/ML/marimo/docs/pyproject.toml
```toml
[build-system]
build-backend = "hatchling.build"
requires = ["hatchling"]
[project]
dependencies = []
name = "marimo_docs"
version = "0.1.0"
[tool.hatch.build.targets.wheel]
include = ["blocks.py"]
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/anthropic_example.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# ]
# ///
import marimo
__generated_with = "0.9.9"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell
def __(mo):
mo.md(
r"""
# Using Anthropic
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat) to make a chatbot backed by Anthropic.
"""
)
return
@app.cell
def __(mo):
import os
os_key = os.environ.get("ANTHROPIC_API_KEY")
input_key = mo.ui.text(label="Anthropic API key", kind="password")
input_key if not os_key else None
return input_key, os, os_key
@app.cell
def __(input_key, mo, os_key):
key = os_key or input_key.value
mo.stop(
not key,
mo.md("Please provide your Anthropic API key in the input field."),
)
return (key,)
@app.cell
def __(key, mo):
chatbot = mo.ui.chat(
mo.ai.llm.anthropic(
"claude-3-5-sonnet-20240620",
system_message="You are a helpful assistant.",
api_key=key,
),
allow_attachments=[
"image/png",
"image/jpeg",
],
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
max_height=400,
)
chatbot
return (chatbot,)
@app.cell
def __(mo):
mo.md("""Access the chatbot's historical messages with [`chatbot.value`](https://docs.marimo.io/api/inputs/chat.html#accessing-chat-history).""")
return
@app.cell
def __(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/custom.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# ]
# ///
import marimo
__generated_with = "0.8.22"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
# Custom chatbot
This example shows how to make a custom chatbot: just supply a function that takes two arguments,
`messages` and `config`, and returns the chatbot's response. This response can be any object; it
doesn't have to be a string!
"""
)
return
@app.cell
def __(mo):
def simple_echo_model(messages, config):
"""This chatbot echoes what the user says."""
# messages is a list of chatbot messages
message = messages[-1]
# Each message has two fields:
# message.role, which may be "user", "assistant", or "system"
# message.content: the content of the message
return f"You said: {messages[-1].content}!"
chatbot = mo.ui.chat(
simple_echo_model,
prompts=["Hello", "How are you?"],
show_configuration_controls=False
)
chatbot
return chatbot, simple_echo_model
@app.cell
def __(mo):
mo.md("""Access the chatbot's historical messages with `chatbot.value`.""")
return
@app.cell
def __(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/dagger_code_interpreter.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "dagger-io==0.14.0",
# "ell-ai==0.0.14",
# "marimo",
# "openai==1.55.0",
# "pydantic==2.8.2",
# ]
# ///
import marimo
__generated_with = "0.9.20"
app = marimo.App(width="medium")
@app.cell(hide_code=True)
def __():
import marimo as mo
import ell
import textwrap
return ell, mo, textwrap
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
# Chatbot code-interpreter with [Dagger](https://dagger.io/)
This example shows how to create a code-interpreter that executes code using [Dagger](https://dagger.io/) so the code is run in an isolated container.
This example requires Docker running on your computer.
"""
)
return
@app.cell(hide_code=True)
def __(mo):
backend = mo.ui.dropdown(["ollama", "openai"], label="Backend", value="openai")
backend
return (backend,)
@app.cell(hide_code=True)
def __(mo):
# OpenAI config
import os
import openai
input_key = mo.ui.text(
label="OpenAI API key",
kind="password",
value=os.environ.get("OPENAI_API_KEY", ""),
)
input_key
return input_key, openai, os
@app.cell(hide_code=True)
def __(backend, input_key, mo):
def _get_open_ai_client():
openai_key = input_key.value
import openai
mo.stop(
not openai_key,
mo.md(
"Please set the `OPENAI_API_KEY` environment variable or provide it in the input field"
),
)
return openai.Client(api_key=openai_key)
def _get_ollama_client():
import openai
return openai.Client(
api_key="ollama",
base_url="http://localhost:11434/v1",
)
client = (
_get_ollama_client()
if backend.value == "ollama"
else _get_open_ai_client()
)
model = "llama3.1" if backend.value == "ollama" else "gpt-4-turbo"
return client, model
@app.cell
def __():
import dagger
return (dagger,)
@app.cell
def __(mo):
files = mo.ui.file(kind="area")
files
return (files,)
@app.cell
def __(mo):
packages = mo.ui.text_area(label="Packages", value="pandas")
packages
return (packages,)
@app.cell
def __(files):
[file.name for file in files.value]
return
@app.cell
def __(dagger, ell, files, mo, packages):
@ell.tool()
async def execute_code(code: str):
"""
Execute python using Dagger. You MUST have print() in the last expression.
"""
async with dagger.Connection() as _dag:
container = (
_dag.container()
.from_("python:3.12-slim")
.with_new_file("/app/script.py", contents=code)
.with_new_file("/app/requirements.txt", contents=packages.value)
.with_exec(["pip", "install", "-r", "/app/requirements.txt"])
)
for file in files.value:
container = container.with_new_file(
f"/app/{file.name}", contents=file.contents.decode("utf-8")
)
result = (
await container.with_workdir("/app")
.with_exec(["python", "/app/script.py"])
.stdout()
)
return mo.vstack(
[
mo.ui.code_editor(code, language="python", disabled=True),
mo.md(result),
]
)
return (execute_code,)
@app.cell(hide_code=True)
def __():
def describe_file(file):
if file.name.endswith(".py"):
return f"Python file: {file.name}"
if file.name.endswith(".txt"):
return f"Text file: {file.name}"
if file.name.endswith(".csv"):
return f"CSV file: {file.name}. Headers: {file.contents.decode('utf-8').splitlines()[0]}"
return f"File: {file.name}"
return (describe_file,)
@app.cell
def __(
client,
describe_file,
ell,
execute_code,
files,
mo,
model,
packages,
):
files_instructions = ""
packages_instructions = ""
if files.value:
files_instructions = f"""
Here are the files you can access:"
{"\n".join([describe_file(file) for file in files.value])}
"""
if packages.value:
packages_instructions = f"""
Here are the python packages you can access:"
{packages.value}
"""
@ell.complex(
model=model,
tools=[execute_code],
client=client,
)
def custom_chatbot(messages, config) -> str:
system_message = ell.system(f"""
You are data scientist with access to writing python code.
{files_instructions}
{packages_instructions}
""")
return [system_message] + [
ell.user(message.content)
if message.role == "user"
else ell.assistant(message.content)
for message in messages
]
def my_model(messages, config):
response = custom_chatbot(messages, config)
if response.tool_calls:
return response.tool_calls[0]()
return mo.md(response.text)
return custom_chatbot, files_instructions, my_model, packages_instructions
@app.cell
def __(mo, my_model):
mo.ui.chat(
my_model,
prompts=[
"What is the square root of {{number}}?",
f"Can you sum this list using python: {list(range(1, 10))}",
],
)
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/gemini.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# ]
# ///
import marimo
__generated_with = "0.8.22"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell
def __(mo):
mo.md(
r"""
# Using Gemini
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat) to make a chatbot backed by Gemini.
"""
)
return
@app.cell
def __(mo):
import os
os_key = os.environ.get("GOOGLE_AI_API_KEY")
input_key = mo.ui.text(label="Google AI API key", kind="password")
input_key if not os_key else None
return input_key, os, os_key
@app.cell
def __(input_key, mo, os_key):
key = os_key or input_key.value
mo.stop(
not key,
mo.md("Please provide your Google AI API key in the input field."),
)
return (key,)
@app.cell
def __(key, mo):
chatbot = mo.ui.chat(
mo.ai.llm.google(
"gemini-1.5-pro-latest",
system_message="You are a helpful assistant.",
api_key=key,
),
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
)
chatbot
return (chatbot,)
@app.cell(hide_code=True)
def __(mo):
mo.md("""Access the chatbot's historical messages with [`chatbot.value`](https://docs.marimo.io/api/inputs/chat.html#accessing-chat-history).""")
return
@app.cell
def __(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/generative_ui.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "ell-ai==0.0.14",
# "marimo",
# "openai==1.53.0",
# "polars==1.12.0",
# ]
# ///
import marimo
__generated_with = "0.9.14"
app = marimo.App(width="medium")
@app.cell
def __():
import polars as pl
import marimo as mo
import os
has_api_key = os.environ.get("OPENAI_API_KEY") is not None
mo.stop(
not has_api_key,
mo.md("Please set the `OPENAI_API_KEY` environment variable").callout(),
)
# Grab a dataset
df = pl.read_csv("hf://datasets/scikit-learn/Fish/Fish.csv")
return df, has_api_key, mo, os, pl
@app.cell
def __(df, mo):
import ell
@ell.tool()
def chart_data(x_encoding: str, y_encoding: str, color: str):
"""Generate an altair chart"""
import altair as alt
return (
alt.Chart(df)
.mark_circle()
.encode(x=x_encoding, y=y_encoding, color=color)
.properties(width=500)
)
@ell.tool()
def filter_dataset(sql_query: str):
"""
Filter a polars dataframe using SQL. Please only use fields from the schema.
When referring to the table in SQL, call it 'data'.
"""
filtered = df.sql(sql_query, table_name="data")
return mo.ui.table(
filtered,
label=f"```sql\n{sql_query}\n```",
selection=None,
show_column_summaries=False,
)
return chart_data, ell, filter_dataset
@app.cell
def __(chart_data, df, ell, filter_dataset, mo):
@ell.complex(model="gpt-4o", tools=[chart_data, filter_dataset])
def analyze_dataset(prompt: str) -> str:
"""You are a data scientist that can analyze a dataset"""
return f"I have a dataset with schema: {df.schema}. \n{prompt}"
def my_model(messages):
response = analyze_dataset(messages)
if response.tool_calls:
return response.tool_calls[0]()
return response.text
mo.ui.chat(
my_model,
prompts=[
"Can you chart two columns of your choosing?",
"Can you find the min, max of all numeric fields?",
"What is the sum of {{column}}?",
],
)
return analyze_dataset, my_model
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/llm_datasette.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "llm==0.16",
# "marimo",
# ]
# ///
import marimo
__generated_with = "0.9.3"
app = marimo.App()
@app.cell(hide_code=True)
def __(mo):
mo.md(r"""## Using <https://llm.datasette.io> with `mo.ui.chat()`""")
return
@app.cell(hide_code=True)
def __(mo):
mo.md("""To set a key, run: `llm keys set openai` in your terminal""")
return
@app.cell
def __():
import marimo as mo
import llm
return llm, mo
@app.cell
def __(llm, mo):
model = llm.get_model("gpt-4o-mini")
conversation = model.conversation()
chat = mo.ui.chat(lambda messages: conversation.prompt(messages[-1].content))
chat
return chat, conversation, model
@app.cell
def __(chat):
chat.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/deepseek_example.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "openai==1.60.2",
# ]
# ///
import marimo
__generated_with = "0.10.17"
app = marimo.App(width="medium")
@app.cell
def _():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
# Using DeepSeek
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat/?h=mo.ui.chat) to make a chatbot backed by [Deepseek](https://deepseek.com/).
"""
)
return
@app.cell(hide_code=True)
def _(mo):
mo.md(
r"""
<a href="https://api-docs.deepseek.com/" target="_blank" rel="noopener noreferrer">
<img
src="https://chat.deepseek.com/deepseek-chat.jpeg"
alt="Powered by deepseek"
width="450"
/>
</a>
"""
).center()
return
@app.cell
def _(mo):
import os
os_key = os.environ.get("DEEPSEEK_API_KEY")
input_key = mo.ui.text(label="Deepseek API key", kind="password")
input_key if not os_key else None
return input_key, os, os_key
@app.cell
def _(input_key, mo, os_key):
key = os_key or input_key.value
mo.stop(
not key,
mo.md("Please provide your Deepseek AI API key in the input field."),
)
return (key,)
@app.cell
def _(key, mo):
chatbot = mo.ui.chat(
mo.ai.llm.openai(
model="deepseek-reasoner",
system_message="You are a helpful assistant.",
api_key=key,
base_url="https://api.deepseek.com",
),
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
)
chatbot
return (chatbot,)
@app.cell(hide_code=True)
def _(mo):
mo.md("""Access the chatbot's historical messages with [`chatbot.value`](https://docs.marimo.io/api/inputs/chat.html#accessing-chat-history).""")
return
@app.cell
def _(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/mlx_chat.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "mlx-lm==0.19.0",
# "huggingface-hub==0.25.1",
# ]
# ///
import marimo
__generated_with = "0.9.4"
app = marimo.App(width="medium")
@app.cell
def __():
from mlx_lm import load, generate
from pathlib import Path
import marimo as mo
from huggingface_hub import snapshot_download
return Path, generate, load, mo, snapshot_download
@app.cell(hide_code=True)
def __(mo):
mo.md(
r"""
# Using MLX with Marimo
## Chat Example
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat) to make a chatbot backed by Apple's MLX, using the `mlx_lm` library and marimo.
[`mlx_lm`](https://github.com/ml-explore/mlx-examples/tree/main/llm) is a library for running large language models on Apple Silicon.
[`mlx`](https://github.com/ml-explore/mlx) is a framework for running machine learning models on Apple Silicon.
Convert your own models to MLX, or find community-converted ones at various quantizations [here](https://huggingface.co/mlx-community).
### Things you can do to improve this example:
- [`prompt caching`](https://github.com/ml-explore/mlx-examples/blob/main/llms/README.md#long-prompts-and-generations)
- completions / notebook mode
- assistant pre-fill
"""
)
return
@app.cell
def __(Path, snapshot_download):
def get_model_path(path_or_hf_repo: str) -> Path:
"""
Ensures the model is available locally. If the path does not exist locally,
it is downloaded from the Hugging Face Hub.
Args:
path_or_hf_repo (str): The local path or Hugging Face repository ID of the model.
Returns:
Path: The path to the model.
"""
model_path = Path(path_or_hf_repo)
if model_path.exists():
return model_path
else:
try:
# If it doesn't exist locally, download it from Hugging Face
return Path(
snapshot_download(
repo_id=path_or_hf_repo,
allow_patterns=[
"*.json",
"*.safetensors",
"*.py",
"tokenizer.model",
"*.tiktoken",
"*.txt",
],
)
)
except Exception as e:
raise ValueError(
f"Error downloading model from Hugging Face: {str(e)}"
)
return (get_model_path,)
@app.cell
def __(mo):
MODEL_ID = mo.ui.text(
label="Hugging Face Model Repo or Local Path",
value="mlx-community/Llama-3.2-3B-Instruct-bf16",
placeholder="Enter huggingfacerepo_id/model_id or local path",
full_width=True,
)
load_model_button = mo.ui.run_button(label="Load Model")
mo.hstack([MODEL_ID, load_model_button])
return MODEL_ID, load_model_button
@app.cell
def __(MODEL_ID, get_model_path, load, load_model_button, mo):
mo.stop(not load_model_button.value, "Click 'Load Model' to proceed")
try:
mo.output.append(
"⏳ Fetching model... This may take a while if downloading from Hugging Face."
)
model_path = get_model_path(MODEL_ID.value)
mo.output.append(f"📁 Model path: {model_path}")
mo.output.append("🔄 Loading model into memory...")
model, tokenizer = load(model_path)
mo.output.append(f"✅ Model loaded successfully!")
except Exception as e:
mo.output.append(f"❌ Error loading model: {str(e)}")
raise
return model, model_path, tokenizer
@app.cell(hide_code=True)
def __(mo):
# Create a text area for the system message
system_message = mo.ui.text_area(
value="You are a helpful AI assistant.",
label="System Message",
full_width=True,
rows=3,
)
system_message # display the system message
return (system_message,)
@app.cell(hide_code=True)
def __(mo):
temp_slider = mo.ui.slider(
start=0.0, stop=2.0, step=0.1, value=0.7, label="Temperature Slider"
)
max_tokens = mo.ui.number(value=512, label="Max Tokens Per Turn")
temp_slider, max_tokens # display the inputs
return max_tokens, temp_slider
@app.cell
def __(
generate,
max_tokens,
mo,
model,
system_message,
temp_slider,
tokenizer,
):
def mlx_chat_model(messages, config):
# Include the system message as the first message
chat_messages = [{"role": "system", "content": system_message.value}]
# Add the rest of the messages
chat_messages.extend(
[{"role": msg.role, "content": msg.content} for msg in messages]
)
# Use the tokenizer's chat template if available
if hasattr(tokenizer, "apply_chat_template") and tokenizer.chat_template:
prompt = tokenizer.apply_chat_template(
chat_messages, tokenize=False, add_generation_prompt=True
)
else:
# Fallback to simple concatenation if no chat template
prompt = "\n".join(
f"{msg['role']}: {msg['content']}" for msg in chat_messages
)
prompt += "\nassistant:"
# Generate the response
response = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=int(max_tokens.value), # Use the max_tokens input
temp=float(temp_slider.value), # Use the temperature slider
)
return response.strip()
# Create the chat interface
chatbot = mo.ui.chat(
mlx_chat_model,
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
)
# Display the chatbot
chatbot
return chatbot, mlx_chat_model
@app.cell(hide_code=True)
def __(mo):
mo.md("""Access the chatbot's historical messages with `chatbot.value`.""")
return
@app.cell
def __(chatbot):
# Display the chat history
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/groq_example.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "groq==0.11.0",
# ]
# ///
import marimo
__generated_with = "0.9.14"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def __(mo):
mo.md(
r"""
# Using Groq
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat) to make a chatbot backed by [Groq](https://groq.com/).
"""
)
return
@app.cell(hide_code=True)
def __(mo):
# Hyperlinking the groq as it is mentioned here - https://console.groq.com/docs/badge
mo.md(
r"""
<a href="https://groq.com" target="_blank" rel="noopener noreferrer">
<img
src="https://groq.com/wp-content/uploads/2024/03/PBG-mark1-color.svg"
alt="Powered by Groq for fast inference."
width="80" height="800"
/>
</a>
"""
).right()
return
@app.cell
def __(mo):
import os
os_key = os.environ.get("GROQ_AI_API_KEY")
input_key = mo.ui.text(label="Groq AI API key", kind="password")
input_key if not os_key else None
return input_key, os, os_key
@app.cell
def __(input_key, mo, os_key):
key = os_key or input_key.value
mo.stop(
not key,
mo.md("Please provide your Groq AI API key in the input field."),
)
return (key,)
@app.cell
def __(key, mo):
chatbot = mo.ui.chat(
mo.ai.llm.groq(
model="llama-3.1-70b-versatile",
system_message="You are a helpful assistant.",
api_key=key,
),
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
)
chatbot
return (chatbot,)
@app.cell(hide_code=True)
def __(mo):
mo.md("""Access the chatbot's historical messages with [`chatbot.value`](https://docs.marimo.io/api/inputs/chat.html#accessing-chat-history).""")
return
@app.cell
def __(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/openai_example.py
```py
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "marimo",
# "openai==1.54.1",
# ]
# ///
import marimo
__generated_with = "0.9.14"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell(hide_code=True)
def __(mo):
mo.md(
r"""
# Using OpenAI
This example shows how to use [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat) to make a chatbot backed by OpenAI.
"""
)
return
@app.cell
def __(mo):
import os
os_key = os.environ.get("OPENAI_API_KEY")
input_key = mo.ui.text(label="OpenAI API key", kind="password")
input_key if not os_key else None
return input_key, os, os_key
@app.cell
def __(input_key, mo, os_key):
openai_key = os_key or input_key.value
mo.stop(
not openai_key,
mo.md("Please set the OPENAI_API_KEY environment variable or provide it in the input field"),
)
return (openai_key,)
@app.cell
def __(mo, openai_key):
chatbot = mo.ui.chat(
mo.ai.llm.openai(
"gpt-4o",
system_message="You are a helpful assistant.",
api_key=openai_key,
),
prompts=[
"Hello",
"How are you?",
"I'm doing great, how about you?",
],
allow_attachments=[
"image/png",
"image/jpeg"
],
)
chatbot
return (chatbot,)
@app.cell
def __(mo):
mo.md("""Access the chatbot's historical messages with [`chatbot.value`](https://docs.marimo.io/api/inputs/chat.html#accessing-chat-history).""")
return
@app.cell
def __(chatbot):
# chatbot.value is the list of chat messages
chatbot.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/recipe_bot.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "openai==1.53.0",
# ]
# ///
import marimo
__generated_with = "0.9.10"
app = marimo.App(width="medium")
@app.cell
def __():
import marimo as mo
chat = mo.ui.chat(
mo.ai.llm.openai(
"gpt-4o",
system_message="""You are a helpful assistant that can
parse my recipe and summarize them for me.
Give me a title in the first line.""",
),
allow_attachments=["image/png", "image/jpeg"],
prompts=["What is the recipe?"],
)
chat
return chat, mo
@app.cell
def __(chat, mo):
mo.stop(not chat.value)
last_message: str = chat.value[-1].content
title = last_message.split("\n")[0]
summary = "\n".join(last_message.split("\n")[1:])
with open(f"{title}.md", "w") as f:
f.write(summary)
mo.status.toast("Receipt summary saved!", description=title)
return f, last_message, summary, title
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/.storybook/main.ts
```ts
import type { StorybookConfig } from "@storybook/react-vite";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))"],
addons: [
"@storybook/addon-links",
"@storybook/addon-essentials",
"@storybook/addon-interactions",
],
framework: {
name: "@storybook/react-vite",
options: {},
},
docs: {
autodocs: "tag",
docsMode: false,
},
};
export default config;
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/README.md
```md
# Chat 💬
These examples show how to make chatbots with marimo, using [`mo.ui.chat`](https://docs.marimo.io/api/inputs/chat.html#marimo.ui.chat).
- `custom.py` shows how to make a custom chatbot.
- `openai_example.py` shows how to make a chatbot powered by OpenAI models.
- `anthropic_example.py` shows how to make a chatbot powered by Anthropic models.
- `gemini.py` shows how to make a chatbot powered by Google models like Gemini.
- `groq_example.py` shows how to make a chatbot powered by Groq models.
- `mlx_chat.py` shows a simple chatbot using local on-device models with Apple's [MLX](https://github.com/ml-explore/mlx), a machine learning framework from Apple that is similar to JAX and PyTorch. This specific example uses the [mlx-lm](https://github.com/ml-explore/mlx-examples/tree/main/llms) library. Note that Apple Silicon chips are required for using MLX.
- `llm_datasette.py` shows how to make a chatbot powered by Simon W's LLM library.
- `dagger_code_interpreter.py` shows how to make a basic code-interpreter chatbot powered by Dagger containers.
- `recipe_bot.py` shows how to make a chatbot that can parse recipes from images.
- `simplemind_example.py` shows how to integrate [simplemind](https://github.com/kennethreitz/simplemind).
- `generative_ui.py` shows how to make a chatbot that can generate UI code.
Chatbot's in marimo are _reactive_: when the chatbot responds with a message,
all other cells referencing the chatbot are automatically run or marked
stale, with the chatbot's response stored in the object's `value` attribute.
You can use this to make notebooks that respond to the chatbot's response
in arbitrary ways. For example, you can make agentic notebooks!
Once you understand the basics, for a more interesting example, check out
[our notebook that lets you talk to any GitHub repo](../../third_party/sage/),
powered by [storia-ai/sage](https://github.com/storia-ai/sage). This example demonstrates advanced usage
of `ui.chat`, using `langchain` to construct a RAG-powered chatbot, served by
an async generator callback function.
## Running examples
The requirements of each notebook are serialized in them as a top-level
comment. Here are the steps to open an example notebook:
1. [Install marimo](https://docs.marimo.io/getting_started/index.html#installation)
2. [Install `uv`](https://github.com/astral-sh/uv/?tab=readme-ov-file#installation)
3. Open an example with `marimo edit --sandbox <notebook.py>`.
> [!TIP]
> The [`--sandbox` flag](https://docs.marimo.io/guides/editor_features/package_management.html) opens the notebook in an isolated virtual environment,
> automatically installing the notebook's dependencies 📦
You can also open notebooks without `uv`, with just `marimo edit <notebook.py>`;
however, you'll need to install the requirements yourself.
```
File: /Users/morganmcguire/ML/marimo/frontend/.storybook/preview.tsx
```tsx
import type { Preview, Decorator } from "@storybook/react";
import "../src/css/index.css";
import "../src/css/app/App.css";
import "./sb.css";
import "tailwindcss/tailwind.css";
import React, { useEffect } from "react";
import { cn } from "../src/utils/cn";
import { TooltipProvider } from "../src/components/ui/tooltip";
import { Toaster } from "../src/components/ui/toaster";
import { TailwindIndicator } from "../src/components/debug/indicator";
const withTheme: Decorator = (Story, context) => {
const theme = context.globals.theme || "light";
useEffect(() => {
document.body.classList.add(theme, `${theme}-theme`);
return () => document.body.classList.remove(theme, `${theme}-theme`);
}, [theme]);
return (
<div className={cn(theme, "p-5")}>
<TooltipProvider>
<Story />
<Toaster />
<TailwindIndicator />
</TooltipProvider>
</div>
);
};
const preview: Preview = {
parameters: {
actions: { argTypesRegex: "^on[A-Z].*" },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
},
globalTypes: {
theme: {
description: "Global theme",
defaultValue: "light",
toolbar: {
title: "Theme",
icon: "circlehollow",
items: ["light", "dark"],
dynamicTitle: true,
},
},
},
decorators: [withTheme],
};
export default preview;
```
File: /Users/morganmcguire/ML/marimo/frontend/.storybook/preview-head.html
```html
<marimo-filename hidden>app.py</marimo-filename>
<marimo-mode data-mode="edit" hidden></marimo-mode>
<marimo-version data-mode="0.1.0" hidden></marimo-version>
<marimo-user-config
data-config='{"completion":{"activate_on_typing":false, "copilot": true},"save":{"autosave":"off","autosave_delay":0},"keymap":{"preset":"default"},"experimental":{}}'
hidden
></marimo-user-config>
<marimo-app-config data-config="{}" hidden></marimo-app-config>
<marimo-server-token data-token="123" hidden></marimo-server-token>
```
File: /Users/morganmcguire/ML/marimo/examples/ai/chat/simplemind_example.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# "simplemind==0.1.3",
# ]
# ///
import marimo
__generated_with = "0.9.14"
app = marimo.App(width="full")
@app.cell(hide_code=True)
def __(mo):
mo.md(r"""## Using [simplemind](https://github.com/kennethreitz/simplemind) with `mo.ui.chat()`""")
return
@app.cell(hide_code=True)
def __():
import marimo as mo
import os
import simplemind as sm
return mo, os, sm
@app.cell(hide_code=True)
def __(__file__, mo, os):
has_set_env = os.environ.get("OPENAI_API_KEY") is not None
mo.md(f"""
Missing OpenAI API key. Re-run this notebook with the following command:
```bash
export OPENAI_API_KEY='sk-'
marimo edit {__file__}
```
""").callout("warn") if not has_set_env else ""
return (has_set_env,)
@app.cell
def __(mo):
get_logs, set_logs = mo.state([], allow_self_loops=True)
return get_logs, set_logs
@app.cell
def __(set_logs, sm):
def add_log(value):
return set_logs(lambda logs: logs + [value])
class LoggingPlugin(sm.BasePlugin):
def pre_send_hook(self, conversation):
add_log(
f"Sending conversation with {len(conversation.messages)} messages"
)
def add_message_hook(self, conversation, message):
add_log(f"Adding message to conversation: {message.text}")
def cleanup_hook(self, conversation):
add_log(
f"Cleaning up conversation with {len(conversation.messages)} messages"
)
def initialize_hook(self, conversation):
add_log("Initializing conversation")
def post_send_hook(self, conversation, response):
add_log(f"Received response: {response.text}")
return LoggingPlugin, add_log
@app.cell
def __(LoggingPlugin, mo, sm):
conversation = sm.create_conversation(
llm_model="gpt-4o", llm_provider="openai"
)
conversation.add_plugin(LoggingPlugin())
def on_message(messages):
conversation.add_message("user", messages[-1].content)
return conversation.send().text
chat = mo.ui.chat(on_message)
return chat, conversation, on_message
@app.cell
def __(chat, get_logs, mo):
logs = list(reversed(get_logs()))
mo.hstack(
[chat, mo.ui.table(logs, selection=None)],
widths="equal",
)
return (logs,)
@app.cell
def __(chat):
chat.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/.storybook/sb.css
```css
.sbdocs-content {
display: flex;
gap: 20px;
flex-direction: column;
}
.sbdocs.sbdocs-wrapper {
@apply bg-background text-foreground;
}
.sbdocs.sbdocs-wrapper h1 {
@apply text-foreground !important;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/bad_button.py
```py
import marimo
__generated_with = "0.0.1"
app = marimo.App()
@app.cell
def __():
import marimo as mo
return mo,
@app.cell
def __(mo):
b = mo.ui.button(value=None, label='Bad button', on_click=lambda v: v + 1)
b
return b,
@app.cell
def __(b):
b.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/cells.py
```py
import marimo
__generated_with = "0.0.5"
app = marimo.App()
@app.cell
def __():
import marimo as mo
mo.md("# Cell 1")
return mo,
@app.cell
def __(mo):
mo.md("# Cell 2")
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/components.py
```py
import marimo
__generated_with = "0.9.15"
app = marimo.App()
@app.cell
def __(mo):
mo.md("""# UI Elements""")
return
@app.cell
def __(basic_ui_elements, mo):
mo.md(
f"""### Basic elements
{basic_ui_elements}
"""
)
return
@app.cell
def __(basic_ui_elements, construct_element, show_element):
selected_element = construct_element(basic_ui_elements.value)
show_element(selected_element)
return (selected_element,)
@app.cell
def __(selected_element, value):
value(selected_element)
return
@app.cell
def __(composite_elements, mo):
mo.md(
f"""### Composite elements
{composite_elements}
"""
)
return
@app.cell
def __(composite_elements, construct_element, show_element):
composite_element = construct_element(composite_elements.value)
show_element(composite_element)
return (composite_element,)
@app.cell
def __(composite_element, value):
value(composite_element)
return
@app.cell
def __(mo):
composite_elements = mo.ui.dropdown(
options=dict(sorted({
'array': mo.ui.array,
'batch': mo.ui.batch,
'dictionary': mo.ui.dictionary,
'form': mo.ui.form,
'reused-in-markdown': 'reused-in-markdown',
'reused-in-json': 'reused-in-json',
}.items())),
)
return (composite_elements,)
@app.cell
def __(mo):
file_button = lambda: mo.ui.file(kind="button")
file_area = lambda: mo.ui.file(kind="area")
basic_ui_elements = mo.ui.dropdown(
options=dict(
sorted(
{
"button": mo.ui.button,
"checkbox": mo.ui.checkbox,
"date": mo.ui.date,
"dropdown": mo.ui.dropdown,
"file button": file_button,
"file area": file_area,
"multiselect": mo.ui.multiselect,
"number": mo.ui.number,
"radio": mo.ui.radio,
"slider": mo.ui.slider,
"switch": mo.ui.switch,
"table": mo.ui.table,
"text": mo.ui.text,
"text_area": mo.ui.text_area,
}.items()
)
),
)
return basic_ui_elements, file_area, file_button
@app.cell
def __(file_area, file_button, mo):
def construct_element(value):
if value == mo.ui.array:
return mo.ui.array([mo.ui.text(), mo.ui.slider(1, 10), mo.ui.date()])
elif value == mo.ui.batch:
return mo.md(
"""
- Name: {name}
- Date: {date}
"""
).batch(name=mo.ui.text(), date=mo.ui.date())
elif value == mo.ui.button:
return mo.ui.button(
value=0, label="click me", on_click=lambda value: value + 1, keyboard_shortcut="Cmd-l"
)
elif value == mo.ui.checkbox:
return mo.ui.checkbox(label="check me")
elif value == mo.ui.date:
return mo.ui.date()
elif value == mo.ui.dictionary:
return mo.ui.dictionary(
{
"slider": mo.ui.slider(1, 10),
"text": mo.ui.text("type something!"),
"array": mo.ui.array(
[
mo.ui.button(value=0, on_click=lambda v: v + 1)
for _ in range(3)
],
label="buttons",
),
}
)
elif value == 'reused-in-markdown':
text = mo.ui.text()
number = mo.ui.number(1, 10)
return mo.md(f"""
Text: {text}
Same Text: {text}
Number: {number}
Same Number: {number}
""")
elif value == 'reused-in-json':
text = mo.ui.text()
number = mo.ui.number(1, 10)
return mo.as_html([text, number, text, number])
elif value == mo.ui.dropdown:
return mo.ui.dropdown(["a", "b", "c"])
elif value == file_button:
return file_button()
elif value == file_area:
return file_area()
elif value == mo.ui.form:
return mo.ui.text_area(placeholder="...").form()
elif value == mo.ui.multiselect:
return mo.ui.multiselect(["a", "b", "c"])
elif value == mo.ui.number:
return mo.ui.number(start=1, stop=10, step=0.5)
elif value == mo.ui.radio:
return mo.ui.radio(["a", "b", "c"], value="a")
elif value == mo.ui.slider:
return mo.ui.slider(start=1, stop=10, step=1)
elif value == mo.ui.switch:
return mo.ui.switch()
elif value == mo.ui.table:
return mo.ui.table(
data=[
{"first_name": "Michael", "last_name": "Scott"},
{"first_name": "Dwight", "last_name": "Schrute"},
],
label="Employees",
)
elif value == mo.ui.text:
return mo.ui.text()
elif value == mo.ui.text_area:
return mo.ui.text_area()
return None
return (construct_element,)
@app.cell
def __(mo):
def show_element(element):
if element is not None:
return mo.hstack([element], justify="center")
return (show_element,)
@app.cell
def __(mo):
def value(element):
def all_values_are_strings(values):
if values is not None and isinstance(values, list):
return all(isinstance(v, str) for v in values)
if element is not None:
v = (
element.value
if not isinstance(element, mo.ui.file)
else element.name()
)
printed_value = (
mo.as_html(v) if not all_values_are_strings(v) else ", ".join(v)
)
return mo.md(
f"""
The element's current value is {printed_value}
"""
)
return (value,)
@app.cell
def __():
import marimo as mo
return (mo,)
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/layouts/layout_grid.grid.json
```json
{
"type": "grid",
"data": {
"columns": 12,
"rowHeight": 40,
"cells": [
{
"position": [0, 0, 5, 2]
},
{
"position": [0, 2, 4, 2]
},
{
"position": [5, 0, 5, 2]
},
{
"position": [3, 4, 3, 2]
},
{
"position": [0, 4, 3, 2]
},
{
"position": null
}
]
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/disabled_cells.py
```py
import marimo
__generated_with = "0.0.5"
app = marimo.App()
@app.cell
def __():
import marimo as mo
mo.md("# Cell 1")
return (mo,)
@app.cell
def __(mo):
mo.md("# Cell 2")
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/kitchen_sink.py
```py
import marimo
__generated_with = "0.1.61"
app = marimo.App()
@app.cell
def __(np, plt):
# Generate some random data
categories = ["A", "B", "C", "D", "E"]
values = np.random.rand(5)
bar = plt.bar(categories, values)
plt.title("Random Bar Chart")
plt.xlabel("Categories")
plt.ylabel("Values")
None
return bar, categories, values
@app.cell
def __(mo):
# options
callout_kind = mo.ui.dropdown(
label="Color",
options=["neutral", "danger", "warn", "success", "info"],
value="info",
)
justify = mo.ui.dropdown(
["start", "center", "end", "space-between", "space-around"],
value="space-between",
label="justify",
)
align = mo.ui.dropdown(
["start", "center", "end", "stretch"], value="center", label="align"
)
gap = mo.ui.number(start=0, step=0.25, stop=2, value=0.25, label="gap")
wrap = mo.ui.checkbox(label="wrap")
return align, callout_kind, gap, justify, wrap
@app.cell
def __(alt, callout_kind, mo, office_characters, vega_datasets):
options = ["Apples", "Oranges", "Pears"]
# inputs
button = mo.ui.button(label="Click me")
checkbox = mo.ui.checkbox(label="check me")
date = mo.ui.date(label="Start Date")
dropdown = mo.ui.dropdown(options=options, value=options[0])
file = mo.vstack([mo.ui.file(kind="button"), mo.ui.file(kind="area")])
multiselect = mo.ui.multiselect(options=options)
number = mo.ui.number(start=1, stop=20, label="Number")
radio = mo.ui.radio(options=options)
slider = mo.ui.slider(start=1, stop=20, label="Slider", value=3)
switch = mo.ui.switch(label="do not disturb")
table = mo.ui.table(data=office_characters, pagination=True)
text_area = mo.ui.text_area(placeholder="Search...", label="Description")
text = mo.ui.text(placeholder="Search...", label="Filter")
refresh = mo.ui.refresh(label="Refresh", options=["1s", "5s", "10s", "30s"])
microphone = mo.ui.microphone(label="Drop a beat!")
chart = mo.ui.altair_chart(
alt.Chart(vega_datasets.data.cars())
.mark_point()
.encode(
x="Horsepower",
y="Miles_per_Gallon",
color="Origin",
)
)
# form
form = mo.ui.text_area(placeholder="...").form()
# callout
callout = mo.callout("This is a callout", kind=callout_kind.value)
# batch
batch = mo.md("{start} → {end}").batch(
start=mo.ui.date(label="Start Date"), end=mo.ui.date(label="End Date")
)
# status
# TODO(akshayka): this is using an internal API since we don't expose the progress bar
progress_bar = mo._plugins.stateless.status._progress.ProgressBar(
title=None, subtitle=None, total=10
)
with mo.status.spinner(title="Hang tight!") as spinner:
pass
# stat
stat = mo.stat(
value="$100.54",
label="Open price",
caption="+2.4%",
direction="increase",
bordered=True,
)
return (
batch,
button,
callout,
chart,
checkbox,
date,
dropdown,
file,
form,
microphone,
multiselect,
number,
options,
progress_bar,
radio,
refresh,
slider,
spinner,
stat,
switch,
table,
text,
text_area,
)
@app.cell
def __(create_wrapper, mo):
# array
wish = mo.ui.text(placeholder="Wish")
create_wrapper(
mo.ui.array([wish] * 3, label="Three wishes"),
"array",
)
return wish,
@app.cell
def __(batch, create_wrapper, mo):
# batch
create_wrapper(
mo.hstack([batch, batch.value]),
"batch",
)
return
@app.cell
def __(create_wrapper, mo, refresh):
# refresh
create_wrapper(mo.hstack([refresh, refresh.value]), "refresh")
return
@app.cell
def __(button, create_wrapper, mo):
# button
create_wrapper(
mo.hstack([button]),
"button",
)
return
@app.cell
def __(checkbox, create_wrapper, mo):
# checkbox
create_wrapper(
mo.hstack([checkbox, mo.md(f"Has value: {checkbox.value}")]),
"checkbox",
)
return
@app.cell
def __(create_wrapper, mo):
# dictionary
first_name = mo.ui.text(placeholder="First name")
last_name = mo.ui.text(placeholder="Last name")
email = mo.ui.text(placeholder="Email", kind="email")
create_wrapper(
mo.ui.dictionary(
{
"First name": first_name,
"Last name": last_name,
"Email": email,
}
),
"dictionary",
)
return email, first_name, last_name
@app.cell
def __(callout, callout_kind, create_wrapper, mo):
create_wrapper(
mo.vstack([callout_kind, callout], align="stretch", gap=0),
"callout",
)
return
@app.cell
def __(create_wrapper, mo):
create_wrapper(
mo.md(
f"""
# Hello world!
## **Hello world!**
## *Hello world!*
## **_Hello world!_**
`marimo` _supports_ **markdown**
> And Blockquotes
```python
# And code
import marimo as mo
import numpy as np
```
- And
- Lists
1. And
2. Ordered
3. Lists
[And](https://www.youtube.com/watch?v=dQw4w9WgXcQ) Links
"""
),
"markdown",
)
return
@app.cell
def __(create_wrapper, mo):
create_wrapper(
mo.md(
r"""
The exponential function $f(x) = e^x$ can be represented as
\[
f(x) = 1 + x + \frac{x^2}{2!} + \frac{x^3}{3!} + \ldots.
\]
"""
),
"latex",
)
return
@app.cell
def __(create_wrapper, file):
create_wrapper(
file,
"file",
)
return
@app.cell
def __(create_wrapper, mo, multiselect):
create_wrapper(
mo.hstack([multiselect, mo.md(f"Has value: {multiselect.value}")]),
"multiselect",
)
return
@app.cell
def __(create_wrapper, dropdown, mo):
create_wrapper(
mo.hstack([dropdown, mo.md(f"Has value: {dropdown.value}")]),
"dropdown",
)
return
@app.cell
def __(create_wrapper, date, mo):
create_wrapper(
mo.hstack([date, mo.md(f"Has value: {date.value}")]),
"date",
)
return
@app.cell
def __(create_wrapper, mo, switch):
create_wrapper(
mo.hstack([switch, mo.md(f"Has value: {switch.value}")]),
"switch",
)
return
@app.cell
def __(create_wrapper, microphone, mo):
# microphone
create_wrapper(
mo.hstack([microphone, mo.audio(microphone.value)]),
"microphone",
)
return
@app.cell
def __(create_wrapper, mo, slider):
create_wrapper(
mo.hstack([slider, mo.md(f"Has value: {slider.value}")]),
"slider",
)
return
@app.cell
def __(create_wrapper, mo):
_src = "https://upload.wikimedia.org/wikipedia/commons/8/8c/Ivan_Ili%C4%87-Chopin_-_Prelude_no._1_in_C_major.ogg"
create_wrapper(
mo.audio(_src),
"audio",
)
return
@app.cell
def __(create_wrapper, mo):
create_wrapper(
mo.pdf(
src="https://arxiv.org/pdf/2104.00282.pdf",
width="100%",
height="200px",
),
"pdf",
)
return
@app.cell
def __(create_wrapper, mo):
_src = (
"https://images.pexels.com/photos/86596/owl-bird-eyes-eagle-owl-86596.jpeg"
)
create_wrapper(
mo.image(src=_src, width=280, rounded=True),
"image",
)
return
@app.cell
def __(create_wrapper, mo, number):
create_wrapper(
mo.hstack([number, mo.md(f"Has value: {number.value}")]),
"number",
)
return
@app.cell
def __(mo):
def create_wrapper(element, key, code=""):
return mo.vstack(
[mo.md(f"## **{key.upper()}**"), element], align="stretch", gap=2
)
return create_wrapper,
@app.cell
def __(create_wrapper, mo, text):
create_wrapper(
mo.hstack([text, mo.md(f"Has value: {text.value}")]),
"text",
)
return
@app.cell
def __(create_wrapper, mo, text_area):
create_wrapper(
mo.hstack([text_area, mo.md(f"Has value: {text_area.value}")]),
"text_area",
)
return
@app.cell
def __(create_wrapper, mo, radio):
create_wrapper(
mo.hstack([radio, mo.md(f"Has value: {radio.value}")]),
"radio",
)
return
@app.cell
def __(create_wrapper, form, mo):
create_wrapper(
mo.hstack([form, mo.md(f"Has value: {form.value}")]),
"form",
)
return
@app.cell
def __(create_wrapper, mo):
create_wrapper(
mo.accordion(
{
"Door 1": mo.md("Nothing!"),
"Door 2": mo.md("Nothing!"),
"Door 3": mo.image(
"https://images.unsplash.com/photo-1524024973431-2ad916746881",
height=150,
),
}
),
"accordion",
)
return
@app.cell
def __(bar, create_wrapper, mo):
create_wrapper(
mo.ui.tabs(
{
"📈 Sales": bar,
"💻 Settings": mo.ui.text(placeholder="Key"),
}
),
"tabs",
)
return
@app.cell
def __(create_wrapper, mo):
tree = mo.tree(
[
"entry",
"another entry",
{"key": [0, mo.ui.slider(1, 10, value=5), 2]},
],
label="A tree of elements.",
)
create_wrapper(
tree,
"tree",
)
return tree,
@app.cell
def __(align, boxes, create_wrapper, gap, justify, mo, wrap):
horizontal = mo.hstack(
boxes,
align=align.value,
justify=justify.value,
gap=gap.value,
wrap=wrap.value,
)
vertical = mo.vstack(
boxes,
align=align.value,
gap=gap.value,
)
stacks = mo.vstack(
[
mo.hstack([justify, align, gap], justify="center"),
horizontal,
mo.md("-----------------------------"),
vertical,
],
align="stretch",
gap=1,
)
create_wrapper(
stacks,
"stacks",
)
return horizontal, stacks, vertical
@app.cell
def __(create_wrapper, table):
create_wrapper(
table, "table", "mo.ui.table(data=office_characters, pagination=True)"
)
return
@app.cell
def __(create_wrapper, spinner):
# spinner
create_wrapper(
spinner,
"spinner",
)
return
@app.cell
def __(create_wrapper, progress_bar):
# progress bar
create_wrapper(
progress_bar,
"progress-bar",
)
return
@app.cell
def __(create_wrapper, stat):
create_wrapper(
stat,
"stat",
)
return
@app.cell
def __(chart, create_wrapper, mo):
create_wrapper(
mo.vstack([chart, mo.ui.table(chart.value)]),
"altair-chart",
)
return
@app.cell
def __(mo):
def create_box(num=1):
box_size = 30 + num * 10
return mo.Html(
f"<div style='min-width: {box_size}px; min-height: {box_size}px; background-color: orange; text-align: center; line-height: {box_size}px'>{str(num)}</div>"
)
boxes = [create_box(i) for i in range(1, 5)]
return boxes, create_box
@app.cell
def __():
office_characters = [
{"first_name": "Michael", "last_name": "Scott"},
{"first_name": "Jim", "last_name": "Halpert"},
{"first_name": "Pam", "last_name": "Beesly"},
{"first_name": "Dwight", "last_name": "Schrute"},
{"first_name": "Angela", "last_name": "Martin"},
{"first_name": "Kevin", "last_name": "Malone"},
{"first_name": "Oscar", "last_name": "Martinez"},
{"first_name": "Stanley", "last_name": "Hudson"},
{"first_name": "Phyllis", "last_name": "Vance"},
{"first_name": "Meredith", "last_name": "Palmer"},
{"first_name": "Creed", "last_name": "Bratton"},
{"first_name": "Ryan", "last_name": "Howard"},
{"first_name": "Kelly", "last_name": "Kapoor"},
{"first_name": "Toby", "last_name": "Flenderson"},
{"first_name": "Darryl", "last_name": "Philbin"},
{"first_name": "Erin", "last_name": "Hannon"},
{"first_name": "Andy", "last_name": "Bernard"},
{"first_name": "Jan", "last_name": "Levinson"},
{"first_name": "David", "last_name": "Wallace"},
{"first_name": "Holly", "last_name": "Flax"},
]
return office_characters,
@app.cell
def __():
import altair as alt
import vega_datasets
import marimo as mo
import matplotlib.pyplot as plt
import numpy as np
return alt, mo, np, plt, vega_datasets
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/layout_grid.py
```py
import marimo
__generated_with = "0.9.14"
app = marimo.App(layout_file="layouts/layout_grid.grid.json")
@app.cell
def __(mo):
mo.md("""# Grid Layout""")
return
@app.cell
def __(mo, search):
mo.md(f"Searching {search.value}")
return
@app.cell
def __(mo):
search = mo.ui.text(label="Search")
search
return (search,)
@app.cell
def __(mo):
mo.md("""text 1""")
return
@app.cell
def __(mo):
mo.md("""text 2""")
return
@app.cell
def __():
import marimo as mo
return (mo,)
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/output.py
```py
import marimo
__generated_with = "0.1.19"
app = marimo.App()
@app.cell
def __():
import marimo as mo
import time
return mo, time
@app.cell
def __(mo, time):
def loop_replace():
for i in range(5):
mo.output.replace(mo.md(f"Loading replace {i}/5"))
time.sleep(.1) # This is long enough to see the replace
def loop_append():
for i in range(5):
mo.output.append(mo.md(f"Loading {i}/5"))
time.sleep(.01) # This should be shorter than the replace
return loop_append, loop_replace
@app.cell
def __(loop_replace, mo):
loop_replace()
mo.md("Replaced!")
return
@app.cell
def __(loop_append, mo):
loop_append()
mo.output.append(mo.md("Appended!"))
return
@app.cell
def __(loop_append, mo):
loop_append()
mo.output.append(mo.md("Cleared!"))
mo.output.clear()
return
@app.cell
def __(loop_append, mo):
loop_append()
mo.output.append(mo.md("Cleared!"))
mo.output.replace(None)
return
@app.cell
def __(mo):
mo.output.append("To be replaced.")
mo.output.replace_at_index(mo.md("Replaced by index!"), 0)
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/layout_grid_max_width.py
```py
import marimo
__generated_with = "0.1.50"
app = marimo.App(layout_file="layouts/layout_grid_max_width.grid.json")
@app.cell
def __(mo):
mo.md("# Grid Layout")
return
@app.cell
def __(mo, search):
mo.md(f"Searching {search.value}")
return
@app.cell
def __(mo):
search = mo.ui.text(label="Search")
search
return search,
@app.cell
def __(mo):
mo.md("text 1")
return
@app.cell
def __(mo):
mo.md("text 2")
return
@app.cell
def __():
import marimo as mo
return mo,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/shutdown.py
```py
import marimo
__generated_with = "0.1.88"
app = marimo.App()
@app.cell
def __():
import marimo as mo
form = mo.ui.text().form()
form
return form, mo
@app.cell
def __(form, mo):
mo.stop(not form.value, "None")
print(form.value[::-1])
form.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/stdin.py
```py
# Copyright 2024 Marimo. All rights reserved.
import marimo
__generated_with = "0.1.77"
app = marimo.App()
@app.cell
def __():
import marimo as mo
return (mo,)
@app.cell
def __():
value = input("what is your name?")
return (value,)
@app.cell
def __(mo, value):
mo.md(f"## 👋 Hi {value}")
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/title.py
```py
import marimo
__generated_with = "0.0.1"
app = marimo.App()
@app.cell
def __():
import marimo as mo
mo.md("# Hello Marimo!")
return mo,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/streams.py
```py
import marimo
__generated_with = "0.0.1"
app = marimo.App()
@app.cell
def __():
import os
return os,
@app.cell
def __():
print('Hello, python!')
return
@app.cell
def __(os):
os.system('echo Hello, stdout!')
os.system('echo Hello, stderr! 1>&2')
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/badButton.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
const appUrl = getAppUrl("bad_button.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
}
});
test("invalid on_click does not crash kernel", async ({ page }) => {
await page.getByRole("button", { name: "Bad button" }).click();
// the kernel should still be alive
await expect(page.getByText("kernel not found")).toHaveCount(0);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/bugs.py
```py
import marimo
__generated_with = "0.0.6"
app = marimo.App()
@app.cell
def __():
import marimo as mo
mo.md("This is an e2e test to capture bugs encountered that we don't want to show up again.")
return mo,
@app.cell
def __(mo):
bug_1 = mo.ui.number(1, 10)
mo.md("bug 1")
return bug_1,
@app.cell
def __(bug_1):
bug_1
return
@app.cell
def __(bug_1):
bug_1.value
return
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/bugs.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
import { createCellBelow, maybeRestartKernel, runCell } from "./helper";
const appUrl = getAppUrl("bugs.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
await maybeRestartKernel(page);
}
});
/**
* This test makes sure that downstream UI elements are re-initialized when
* upstream source cells are re-run.
*/
test("correctly initializes cells", async ({ page }, info) => {
// Is initialized to 1
const number = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
await expect(number).toBeVisible();
await expect(number.inputValue()).resolves.toBe("1");
// Change the value to 5
await number.fill("5");
await number.blur();
// Create a new cell, add `bug_1` to it, and run it
await createCellBelow({
page,
cellSelector: "text=bug 1",
content: "bug_1",
run: true,
});
// Check they are both 5
let numberInputs = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
await expect(numberInputs).toHaveCount(2);
await expect(numberInputs.first()).toHaveValue("5");
await expect(numberInputs.last()).toHaveValue("5");
// Run first cell
await runCell({ page, cellSelector: "text=bug 1" });
// Check each number input is 1
numberInputs = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
await expect(numberInputs).toHaveCount(2);
await expect(numberInputs.first()).toHaveValue("1");
await expect(numberInputs.last()).toHaveValue("1");
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/components.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect, type Page } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
import { takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
const appUrl = getAppUrl("components.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
}
});
// This can run fully parallel since its in run mode
test.describe.configure({ mode: "parallel" });
const pageHelper = (page: Page) => {
return {
cell(index: number) {
return page.locator(".Cell").nth(index);
},
async selectBasicComponent(type: string) {
const select = await this.cell(1).locator("select");
await select.selectOption({ label: type });
},
async selectComplexComponent(type: string) {
const select = await this.cell(4).locator("select");
await select.selectOption({ label: type });
},
async verifyOutput(text: string) {
await expect(
page.getByText(`The element's current value is ${text}`),
).toBeVisible();
},
};
};
test("page renders read only view in read mode", async ({ page }) => {
// Filename is not visible
await expect(page.getByText("components.py").last()).not.toBeVisible();
// Has elements with class name 'controls'
await expect(page.locator("#save-button")).toHaveCount(0);
// Can see output
await expect(page.locator("h1").getByText("UI Elements")).toBeVisible();
await takeScreenshot(page, _filename);
});
test("button", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("button");
const element = page.locator("button").getByText("click me");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("0");
// Click button
await element.click();
// Verify output
await helper.verifyOutput("1");
await takeScreenshot(page, _filename);
});
test("checkbox", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("checkbox");
const element = page.getByText("check me");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("False");
// Click checkbox
await element.click();
// Verify output
await helper.verifyOutput("True");
// Click checkbox
await element.click();
// Verify output
await helper.verifyOutput("False");
await takeScreenshot(page, _filename);
});
test.skip("date", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("date");
const element = page.getByRole("textbox");
// Verify is visible
await expect(element).toBeVisible();
await element.fill("2020-01-20");
// Verify output
await helper.verifyOutput("2020-01-20");
await takeScreenshot(page, _filename);
});
test("dropdown", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("dropdown");
const element = helper.cell(2).getByRole("combobox");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("None");
// Select option
await element.selectOption({ label: "b" });
// Verify output
await helper.verifyOutput("b");
await takeScreenshot(page, _filename);
});
test("file button", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("file button");
const element = page.getByRole("button", { name: "Upload", exact: true });
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("None");
await takeScreenshot(page, _filename);
});
test("file area", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("file area");
const element = page.getByText("Drag and drop files here");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("None");
await takeScreenshot(page, _filename);
});
test("multiselect", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("multiselect");
let element = page.locator("marimo-multiselect div svg").last();
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("");
// Select option
await element.click();
await page.getByText("b", { exact: true }).click();
// Verify output
await helper.verifyOutput("b");
// Select option
element = page.locator("marimo-multiselect div svg").last();
await element.click();
await page.getByText("c", { exact: true }).click();
// Verify output
await helper.verifyOutput("b, c");
await takeScreenshot(page, _filename);
});
test("number", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("number");
const element = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("1");
// Select option
await element.fill("5");
await element.first().blur();
// Verify output
await helper.verifyOutput("5");
await takeScreenshot(page, _filename);
});
test("radio", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("radio");
const element = page.getByRole("radiogroup").getByText("a");
// Verify is visible and selected
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("a");
// Select option
await page.getByRole("radiogroup").getByText("b").click();
// Verify a is not selected
// Verify output
await helper.verifyOutput("b");
await takeScreenshot(page, _filename);
});
test("slider", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("slider");
const element = page.getByRole("slider");
// Verify is visible and selected
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("1");
// Move slider
await element.dragTo(page.getByTestId("track"));
// Verify output
await helper.verifyOutput("6");
await takeScreenshot(page, _filename);
});
test("switch", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("switch");
const element = page.getByRole("switch");
// Verify is visible
await expect(element).toBeVisible();
// Verify output
await helper.verifyOutput("False");
// Click checkbox
await element.click();
// Verify output
await helper.verifyOutput("True");
// Click checkbox
await element.click();
// Verify output
await helper.verifyOutput("False");
await takeScreenshot(page, _filename);
});
test("table", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("table");
const element = page.getByText("Michael");
// Verify is visible
await expect(element).toBeVisible();
// Click first checkbox to select all
await page.getByRole("checkbox").first().click();
await expect(
helper.cell(3).locator(".marimo-json-output").first(),
).toHaveText(
`
[2 Items
0:{2 Items
"first_name":"Michael"
"last_name":"Scott"
}
1:{2 Items
"first_name":"Dwight"
"last_name":"Schrute"
}
]
`.trim(),
{ useInnerText: true },
);
// Click second checkbox to remove first row
await page.getByRole("checkbox").nth(1).click();
await expect(
helper.cell(3).locator(".marimo-json-output").first(),
).toHaveText(
`
[1 Item
0:{2 Items
"first_name":"Dwight"
"last_name":"Schrute"
}
]
`.trim(),
{ useInnerText: true },
);
await takeScreenshot(page, _filename);
});
test("text", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("text");
const element = page.getByRole("textbox");
// Verify is visible
await expect(element).toBeVisible();
// Select option
await element.fill("hello");
// Blur
await element.first().blur();
// Verify output
await helper.verifyOutput("hello");
await takeScreenshot(page, _filename);
});
test("text_area", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectBasicComponent("text_area");
const element = page.getByRole("textbox");
// Verify is visible
await expect(element).toBeVisible();
// Select option
await element.fill("hello");
// Blur
await element.first().blur();
// Verify output
await helper.verifyOutput("hello");
await takeScreenshot(page, _filename);
});
test.skip("complex - array", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("array");
// Check the elements
const textbox = page.getByRole("textbox").first();
const slider = page.getByRole("slider");
const date = page.getByRole("textbox").last();
// Verify they are visible
await expect(textbox).toBeVisible();
await expect(slider).toBeVisible();
await expect(date).toBeVisible();
// Fill
await textbox.fill("hi marimo");
await slider.dragTo(page.getByTestId("track").first());
await date.fill("2020-01-20");
// Verify output
await expect(
helper.cell(6).locator(".marimo-json-output").first(),
).toHaveText(
`
[3 Items
0:"hi marimo"
1:5
2:2020-01-20
]
`.trim(),
{ useInnerText: true },
);
await takeScreenshot(page, _filename);
});
test.skip("complex - batch", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("batch");
// Check the elements
const textbox = page.getByRole("textbox").first();
const date = page.getByRole("textbox").last();
// Verify they are visible
await expect(textbox).toBeVisible();
await expect(date).toBeVisible();
// Fill
await textbox.fill("hi again marimo");
await date.fill("2020-04-20");
// Verify output
await expect(
helper.cell(6).locator(".marimo-json-output").first(),
).toHaveText(
`
{2 Items
"name":"hi again marimo"
"date":2020-04-20
}
`.trim(),
{ useInnerText: true },
);
await takeScreenshot(page, _filename);
});
test("complex - dictionary", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("dictionary");
// Check the elements
const textbox = page.getByRole("textbox").first();
const buttons = page.locator("button:visible").getByText("click here");
// Verify they are visible
await expect(textbox).toBeVisible();
await expect(buttons).toHaveCount(3);
// Fill
await textbox.fill("something!");
// Click first button twice
await buttons.first().click();
await buttons.first().click();
// Click last once
await buttons.last().click();
// Verify output
await expect(
helper.cell(6).locator(".marimo-json-output").first(),
).toHaveText(
`
{3 Items
"slider":1
"text":"something!"
"array":[3 Items
0:2
1:0
2:1
]
}
`.trim(),
{ useInnerText: true },
);
await takeScreenshot(page, _filename);
});
test("complex - form", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("form");
// Check the elements
const texteara = page.locator("textarea:visible");
// Verify they are visible
await expect(texteara).toBeVisible();
// Verify no output
await helper.verifyOutput("None");
// Fill
await texteara.fill("something!");
// Verify output is still empty until submit
await helper.verifyOutput("None");
// Click submit
await page.locator("button:visible").getByText("Submit").click();
// Verify output
await helper.verifyOutput("something!");
await takeScreenshot(page, _filename);
});
test("complex - reused in json", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("reused-in-json");
// Check the elements
const textbox = page.getByTestId("marimo-plugin-text-input");
const number = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
// Verify they are visible
await expect(textbox).toHaveCount(2);
await expect(number).toHaveCount(2);
// Fill the first one
await textbox.first().fill("hello");
await number.first().fill("5");
await number.first().blur();
// Verify all have the same value
await expect(textbox.last()).toHaveValue("hello");
await expect(number.last()).toHaveValue("5");
// Fill the last one
await textbox.last().fill("world");
await number.last().fill("10");
await number.last().blur();
// Verify all have the same value
await expect(textbox.first()).toHaveValue("world");
await expect(number.first()).toHaveValue("10");
await takeScreenshot(page, _filename);
});
test("complex - reused in markdown", async ({ page }) => {
const helper = pageHelper(page);
await helper.selectComplexComponent("reused-in-markdown");
// Check the elements
const textbox = page.getByTestId("marimo-plugin-text-input");
const number = page
.getByTestId("marimo-plugin-number-input")
.locator("input");
// Verify they are visible
await expect(textbox).toHaveCount(2);
await expect(number).toHaveCount(2);
// Fill the first one
await textbox.first().fill("hello");
await number.first().fill("5");
await number.first().blur();
// Verify all have the same value
await expect(textbox.last()).toHaveValue("hello");
await expect(number.last()).toHaveValue("5");
// Fill the last one
await textbox.last().fill("world");
await number.last().fill("10");
await number.last().blur();
// Verify all have the same value
await expect(textbox.first()).toHaveValue("world");
await expect(number.first()).toHaveValue("10");
await takeScreenshot(page, _filename);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/py/layouts/layout_grid_max_width.grid.json
```json
{
"type": "grid",
"data": {
"columns": 12,
"rowHeight": 40,
"maxWidth": 800,
"bordered": true,
"cells": [
{
"position": [0, 0, 5, 2]
},
{
"position": [0, 2, 4, 2]
},
{
"position": [5, 0, 5, 2]
},
{
"position": [3, 4, 3, 2]
},
{
"position": [0, 4, 3, 2]
},
{
"position": null
}
]
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/disabled.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl, resetFile } from "../playwright.config";
import { maybeRestartKernel, takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
const appUrl = getAppUrl("disabled_cells.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
await maybeRestartKernel(page);
}
});
test.afterEach(async () => {
// Need to reset the file because this test modifies it
await resetFile("disabled_cells.py");
});
test("disabled cells", async ({ page }) => {
// Can see output / code
await expect(page.locator("h1").getByText("Cell 1")).toBeVisible();
await expect(page.locator("h1").getByText("Cell 2")).toBeVisible();
// No add buttons are visible
await expect(
page.getByTestId("create-cell-button").locator(":visible"),
).toHaveCount(0);
// Hover over a cell the drag button button appears
// Click the drag button and then disable the cell
await page.hover("text=Cell 1");
await page.getByTestId("drag-button").locator(":visible").first().click();
await page.getByText("Disable cell").click();
// Check the cell status
await expect(page.getByTitle("This cell is disabled")).toBeVisible();
await expect(
page.getByTitle("This cell has a disabled ancestor"),
).toBeVisible();
await expect(
page
.getByTestId("cell-status")
.first()
.evaluate((el) => el.dataset.status),
).resolves.toBe("disabled");
await expect(
page
.getByTestId("cell-status")
.last()
.evaluate((el) => el.dataset.status),
).resolves.toBe("disabled-transitively");
// Add code to the first cell and save
// The result should be stale
await page.click(".cm-editor");
await page.keyboard.type("\nx = 2");
await page.getByTestId("run-button").locator(":visible").first().click();
await expect(page.getByTitle("This cell is disabled")).toBeVisible();
await expect(
page.getByTitle("This cell has a disabled ancestor"),
).toBeVisible();
await expect(
page
.getByTestId("cell-status")
.first()
.evaluate((el) => el.dataset.status),
).resolves.toBe("stale");
await expect(
page
.getByTestId("cell-status")
.last()
.evaluate((el) => el.dataset.status),
).resolves.toBe("stale");
// Disable the second cell
await page.hover("text=Cell 2");
await page.getByTestId("drag-button").locator(":visible").first().click();
await page.getByText("Disable cell").click();
// Check they are still stale
await expect(page.getByTitle("This cell is disabled")).toHaveCount(2);
await expect(
page
.getByTestId("cell-status")
.first()
.evaluate((el) => el.dataset.status),
).resolves.toBe("stale");
await expect(
page
.getByTestId("cell-status")
.last()
.evaluate((el) => el.dataset.status),
).resolves.toBe("stale");
// Enable the first
await page.hover("text=Cell 1");
await page.getByTestId("drag-button").locator(":visible").first().click();
await page.getByText("Enable cell").click();
// Check the status
await expect(page.getByTitle("This cell is disabled")).toHaveCount(1);
// Enable the second
await page.hover("text=Cell 2");
await page.getByTestId("drag-button").locator(":visible").first().click();
await page.getByText("Enable cell").click();
// Check the status
await expect(page.getByTitle("This cell is disabled")).toHaveCount(0);
await takeScreenshot(page, _filename);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/helper.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { type Locator, type Page, expect } from "@playwright/test";
import { HotkeyProvider, type HotkeyAction } from "../src/core/hotkeys/hotkeys";
import path from "node:path";
export async function createCellBelow(opts: {
page: Page;
cellSelector: string;
content: string;
run: boolean;
}) {
const { page, cellSelector, content, run } = opts;
// Hover over a cell the 'add cell' button appears
await page.hover(cellSelector);
await expect(
page.getByTestId("create-cell-button").locator(":visible"),
).toHaveCount(2);
// Clicking the first button creates a new cell below
await page
.getByTestId("create-cell-button")
.locator(":visible")
.last()
.click();
// Type into the currently focused cell
if (content) {
await page.locator("*:focus").type(content);
}
// Run the new cell
if (run) {
await page.locator("*:focus").hover();
await page.getByTestId("run-button").locator(":visible").first().click();
}
}
export async function openCellActions(page: Page, element: Locator) {
await element.hover();
await page
.getByTestId("cell-actions-button")
.locator(":visible")
.first()
.click();
}
export async function runCell(opts: { page: Page; cellSelector: string }) {
const { page, cellSelector } = opts;
// Hover over a cell
await page.hover(cellSelector);
// Run the new cell
await page.getByTestId("run-button").locator(":visible").first().click();
}
const countsForName: Record<string, number> = {};
/**
* Take a screenshot of the page.
* @example
* await takeScreenshot(page, _filename);
*/
export async function takeScreenshot(page: Page, filename: string) {
const clean = path.basename(filename).replace(".spec.ts", "");
const count = countsForName[clean] || 0;
countsForName[clean] = count + 1;
const fullName = `${clean}.${count}`;
await page.screenshot({
path: `e2e-tests/screenshots/${fullName}.png`,
fullPage: true,
});
}
/**
* Press a hotkey on the page.
*
* It uses the hotkey provider to get the correct key for the current platform
* and then maps it to the correct key for playwright.
*/
export async function pressShortcut(page: Page, action: HotkeyAction) {
const isMac = await page.evaluate(() => navigator.userAgent.includes("Mac"));
const provider = HotkeyProvider.create(isMac);
const key = provider.getHotkey(action);
// playwright uses "Meta" for command key on mac, "Control" for windows/linux
// we also need to capitalize the first letter of each key
const split = key.key.split("-");
const capitalized = split.map((s) => s[0].toUpperCase() + s.slice(1));
const keymap = capitalized
.join("+")
.replace("Cmd", isMac ? "Meta" : "Control")
.replace("Ctrl", "Control");
await page.keyboard.press(keymap);
}
/**
* Download as HTML
*
* Download HTML of the current notebook and take a screenshot
*/
export async function exportAsHTMLAndTakeScreenshot(page: Page) {
// Wait for networkidle so that the notebook is fully loaded
await page.waitForLoadState("networkidle");
// Start waiting for download before clicking.
const [download] = await Promise.all([
page.waitForEvent("download"),
page
.getByTestId("notebook-menu-dropdown")
.click()
.then(() => {
return page.getByText("Download", { exact: true }).hover();
})
.then(() => {
return page.getByText("Download as HTML", { exact: true }).click();
}),
]);
// Wait for the download process to complete and save the downloaded file somewhere.
const path = `e2e-tests/exports/${download.suggestedFilename()}`;
await download.saveAs(path);
// Open a new page and take a screenshot
const exportPage = await page.context().newPage();
const fullPath = `${process.cwd()}/${path}`;
await exportPage.goto(`file://${fullPath}`, {
waitUntil: "networkidle",
});
await takeScreenshot(exportPage, path);
// Toggle code
if (await exportPage.isVisible("[data-testid=show-code]")) {
await exportPage.getByTestId("show-code").click();
// wait 100ms for the code to be shown
await exportPage.waitForTimeout(100);
}
// Take screenshot of code
await takeScreenshot(exportPage, `code-${path}`);
}
export async function exportAsPNG(page: Page) {
// Wait for networkidle so that the notebook is fully loaded
await page.waitForLoadState("networkidle");
const [download] = await Promise.all([
page.waitForEvent("download"),
page
.getByTestId("notebook-menu-dropdown")
.click()
.then(() => {
return page.getByText("Download", { exact: true }).hover();
})
.then(() => {
return page.getByText("Download as PNG", { exact: true }).click();
}),
]);
// Wait for the download process to complete and save the downloaded file somewhere.
const path = `e2e-tests/screenshots/${download.suggestedFilename()}`;
await download.saveAs(path);
}
/**
* Waits for the page to load. If we have resumed a session, we restart the kernel.
*/
export async function maybeRestartKernel(page: Page) {
// Wait for cells to appear
await waitForCellsToRender(page);
// If it says, "You have connected to an existing session", then restart
const hasText = await page
.getByText("You have reconnected to an existing session", { exact: false })
.isVisible();
if (!hasText) {
return;
}
await page.getByTestId("notebook-menu-dropdown").click();
await page.getByText("Restart kernel", { exact: true }).click();
await page.getByLabel("Confirm Restart", { exact: true }).click();
}
/**
* Waits for cells to render in edit mode.
*/
export async function waitForCellsToRender(page: Page) {
await page.waitForSelector("[data-testid=cell-editor]");
}
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/cells.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl, resetFile } from "../playwright.config";
import {
exportAsHTMLAndTakeScreenshot,
pressShortcut,
maybeRestartKernel,
} from "./helper";
const appUrl = getAppUrl("cells.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
await maybeRestartKernel(page);
}
});
test.afterEach(async () => {
// Need to reset the file because this test modifies it
await resetFile("cells.py");
});
/**
* Cell re-render count is a good indicator of performance.
*/
test("keeps re-renders from growing", async ({ page }) => {
await page.waitForLoadState("networkidle");
// Read the render count
const cellRenderCount = await page.evaluate(
() => document.body.dataset.cellRenderCount,
);
// This count may grow with the addition of new features. If this is the case,
// it is okay to increase the count. However, if the count is growing
// unexpectedly, it is a sign that something is causing cells to re-render.
// It is also ok to decrease the count if we find a way to reduce the number
// of renders.
expect(cellRenderCount).toBeDefined();
expect(Number.parseInt(cellRenderCount || "")).toBeLessThanOrEqual(6);
});
/**
* This tests:
* - adding cells above and below existing cells.
* - running individual cells
* - moving cells up and down
*/
test("page renders 2 cells", async ({ page }) => {
// Can see output / code
await expect(page.locator("h1").getByText("Cell 1")).toBeVisible();
await expect(page.locator("h1").getByText("Cell 2")).toBeVisible();
// No add buttons are visible
await expect(
page.getByTestId("create-cell-button").locator(":visible"),
).toHaveCount(0);
// Hover over a cell the 'add cell' button appears
await page.hover("text=Cell 1");
await expect(
page.getByTestId("create-cell-button").locator(":visible"),
).toHaveCount(2);
// Clicking the first button creates a new cell at the top
await page
.getByTestId("create-cell-button")
.locator(":visible")
.first()
.click();
// Type into the currently focused cell
await page.locator("*:focus").fill(`mo.md("# Cell 0")`);
// Check the rendered cells
await expect(page.locator("h1")).toHaveText(["Cell 1", "Cell 2"]);
// Run the new cell
await page.getByTestId("run-button").locator(":visible").first().click();
// Check the rendered cells
await expect(page.locator("h1")).toHaveText(["Cell 0", "Cell 1", "Cell 2"]);
// Add cell below, text, and run
await page.hover("text=Cell 1");
await page
.getByTestId("create-cell-button")
.locator(":visible")
.last()
.click();
await page.locator("*:focus").fill(`mo.md("# Cell 1.5")`);
await page.getByTestId("run-button").locator(":visible").last().click();
// Verify the rendered cells
await expect(page.locator("h1")).toHaveText([
"Cell 0",
"Cell 1",
"Cell 1.5",
"Cell 2",
]);
// Focus on Cell 0 and move it down
await page
.getByRole("textbox")
.filter({ hasText: 'mo.md("# Cell 0")' })
.click();
await pressShortcut(page, "cell.moveDown");
// Focus on Cell 2 and move it up
await page
.getByRole("textbox")
.filter({ hasText: 'mo.md("# Cell 2")' })
.click();
await pressShortcut(page, "cell.moveUp");
// Verify the rendered cells
await expect(page.locator("h1")).toHaveText([
"Cell 1",
"Cell 0",
"Cell 2",
"Cell 1.5",
]);
// Revert the file by deleting the new cells
// Delete cell 1.5
await page
.getByRole("textbox")
.filter({ hasText: 'mo.md("# Cell 1.5")' })
.selectText();
await page.keyboard.press("Backspace");
await pressShortcut(page, "cell.delete");
// Delete cell 0
await page
.getByRole("textbox")
.filter({ hasText: 'mo.md("# Cell 0")' })
.selectText();
await page.keyboard.press("Backspace");
await pressShortcut(page, "cell.delete");
// Verify the rendered cells
await expect(page.locator("h1")).toHaveText(["Cell 1", "Cell 2"]);
});
test("download as HTML", async ({ page }) => {
await exportAsHTMLAndTakeScreenshot(page);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/kitchen-sink-wasm.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { expect, test } from "@playwright/test";
import { exportAsHTMLAndTakeScreenshot, takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
test.skip("can screenshot and download as html edit", async ({ page }) => {
await page.goto("http://localhost:3000");
// See text Initializing
await expect(page.getByText("Initializing")).toBeVisible();
// See text Welcome
await expect(page.getByText("Welcome").first()).toBeVisible();
await takeScreenshot(page, _filename);
await exportAsHTMLAndTakeScreenshot(page);
});
test.skip("can screenshot and download as html in run", async ({ page }) => {
await page.goto("http://localhost:3000?mode=read");
// See text Initializing
await expect(page.getByText("Initializing")).toBeVisible();
// See text Welcome
await expect(page.getByText("Welcome").first()).toBeVisible();
await takeScreenshot(page, _filename);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/layout-grid.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect, type Page } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
import { takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
const runUrl = getAppUrl("layout_grid.py//run");
const runMaxWidthUrl = getAppUrl("layout_grid_max_width.py//run");
const editUrl = getAppUrl("layout_grid.py");
test("can run Grid layout", async ({ page }) => {
await page.goto(runUrl);
// wait 500ms to render
await page.waitForTimeout(500);
// Verify markdown "Grid Layout"
await expect(page.getByText("Grid Layout")).toBeVisible();
// Type in search box
await page.getByRole("textbox").last().fill("hello");
// Blur
await page.getByRole("textbox").last().blur();
// Verify dependent output updated
await expect(page.getByText("Searching hello")).toBeVisible();
// Verify text 1 have the same y coordinate as text 2, but text 2 is further left
const bb1 = await bbForText(page, "text 1");
const bb2 = await bbForText(page, "text 2");
expect(bb1.y).toBe(bb2.y);
expect(bb1.x).toBeGreaterThan(bb2.x);
await takeScreenshot(page, _filename);
});
test("can run Grid layout with max-width", async ({ page }) => {
await page.goto(runMaxWidthUrl);
// wait 500ms to render
await page.waitForTimeout(500);
// Verify markdown "Grid Layout"
await expect(page.getByText("Grid Layout")).toBeVisible();
await takeScreenshot(page, _filename);
});
test("can edit Grid layout", async ({ page }) => {
await page.goto(editUrl);
// Verify text 1 bounding box is above text 2
let bb1 = await bbForText(page, "text 1");
let bb2 = await bbForText(page, "text 2");
expect(bb1.y).toBeLessThan(bb2.y);
// Toggle preview-button
await page.locator("#preview-button").click();
// Wait 500ms to allow preview to render
await page.waitForTimeout(500);
// Verify text 1 have the same y coordinate as text 2, but text 2 is further left
bb1 = await bbForText(page, "text 1");
bb2 = await bbForText(page, "text 2");
expect(bb1.y).toBe(bb2.y);
expect(bb1.x).toBeGreaterThan(bb2.x);
// Can still use interactive elements
await page.getByRole("textbox").last().fill("hello");
await page.getByRole("textbox").last().blur();
await expect(page.getByText("Searching hello")).toBeVisible();
// Can toggle to Vertical layout
const layoutSelect = page.getByTestId("layout-select");
await expect(layoutSelect).toBeVisible();
await layoutSelect.click();
await page.getByText("Vertical").click();
// Wait 500ms to allow preview to render
await page.waitForTimeout(500);
// Verify bounding boxes are back to vertical
bb1 = await bbForText(page, "text 1");
bb2 = await bbForText(page, "text 2");
expect(bb1.x).toBe(bb2.x);
expect(bb1.y).toBeLessThan(bb2.y);
await takeScreenshot(page, _filename);
});
interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}
function expectValidBoundingBox(
bb: BoundingBox | null,
): asserts bb is BoundingBox {
expect(bb).toBeDefined();
if (!bb) {
throw new Error("bb is null");
}
expect(bb.x).toBeGreaterThan(0);
expect(bb.y).toBeGreaterThan(0);
expect(bb.width).toBeGreaterThan(0);
expect(bb.height).toBeGreaterThan(0);
}
async function bbForText(page: Page, text: string) {
const el = page.getByText(text).first();
await expect(el).toBeVisible();
const bb = await el.boundingBox();
expectValidBoundingBox(bb);
return bb;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/mode.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect, type Page, type BrowserContext } from "@playwright/test";
import {
type ApplicationNames,
getAppMode,
getAppUrl,
} from "../playwright.config";
import { maybeRestartKernel, takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
async function gotoPage(
app: ApplicationNames,
page: Page,
context: BrowserContext,
) {
const url = getAppUrl(app);
const mode = getAppMode(app);
await page.goto(url);
// Verify is has loaded
if (mode === "edit") {
await maybeRestartKernel(page);
}
}
// Re-use page for all tests
let page: Page;
test.beforeAll(async ({ browser }) => {
page = await browser.newPage();
});
test.afterAll(async () => {
await page.close();
});
test.skip("page renders edit feature in edit mode", async ({ context }) => {
await gotoPage("title.py", page, context);
// 'title.py' to be in the document.
expect(await page.getByText("title.py").count()).toBeGreaterThan(0);
// Has elements with class name 'controls'
expect(page.locator("#save-button")).toBeVisible();
// Can see output
await expect(page.locator("h1").getByText("Hello Marimo!")).toBeVisible();
await takeScreenshot(page, _filename);
});
test.skip("can bring up the find/replace dialog", async ({ context }) => {
await gotoPage("title.py", page, context);
// Wait for the cells to load
await expect(page.locator("h1").getByText("Hello Marimo!")).toBeVisible();
// Click mod+f to bring up the find/replace dialog
// TODO: This is not working
await page.keyboard.press("Meta+f", { delay: 200 });
// Has placeholder text "Find"
await expect(page.locator("[placeholder='Find']")).toBeVisible();
await takeScreenshot(page, _filename);
});
test("can toggle to presenter mode", async ({ context }) => {
await gotoPage("title.py", page, context);
// Can see output and code
await expect(page.locator("h1").getByText("Hello Marimo!")).toBeVisible();
await expect(page.getByText("# Hello Marimo!")).toBeVisible();
// Toggle preview-button
await page.locator("#preview-button").click();
// Can see output
await expect(page.locator("h1").getByText("Hello Marimo!")).toBeVisible();
// No code
await expect(page.getByText("# Hello Marimo!")).not.toBeVisible();
// Toggle preview-button again
await page.locator("#preview-button").click();
// Can see output and code
await expect(page.locator("h1").getByText("Hello Marimo!")).toBeVisible();
await expect(page.getByText("# Hello Marimo!")).toBeVisible();
await takeScreenshot(page, _filename);
});
test("page renders read only view in read mode", async ({ context }) => {
await gotoPage("components.py", page, context);
// Filename is not visible
await expect(page.getByText("components.py").last()).not.toBeVisible();
// Has elements with class name 'controls'
await expect(page.locator("#save-button")).toHaveCount(0);
// Can see output
await expect(page.locator("h1").getByText("UI Elements")).toBeVisible();
await takeScreenshot(page, _filename);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/kitchen-sink.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
import {
exportAsHTMLAndTakeScreenshot,
exportAsPNG,
takeScreenshot,
} from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
const appUrl = getAppUrl("kitchen_sink.py");
test("can screenshot and download as html", async ({ page }) => {
await page.goto(appUrl);
await takeScreenshot(page, _filename);
await exportAsHTMLAndTakeScreenshot(page);
});
test.skip("can screenshot and download as png", async ({ page }) => {
await page.goto(appUrl);
await exportAsPNG(page);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/shutdown.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl, startServer } from "../playwright.config";
import { takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
test("can resume a session", async ({ page }) => {
const appUrl = getAppUrl("shutdown.py");
await page.goto(appUrl);
await expect(page.getByText("'None'", { exact: true })).toBeVisible();
// type in the form
await page.locator("#output-Hbol").getByRole("textbox").fill("12345");
// shift enter to run the form
await page.keyboard.press("Meta+Enter");
// wait for the output to appear
let secondCell = await page.locator(".Cell").nth(1);
await expect(secondCell.getByText("12345")).toBeVisible();
await expect(secondCell.getByText("54321")).toBeVisible();
// Refresh the page
await page.reload();
await expect(
page.getByText("You have reconnected to an existing session."),
).toBeVisible();
secondCell = await page.locator(".Cell").nth(1);
await expect(page.getByText("12345")).toBeVisible();
await expect(page.getByText("54321")).toBeVisible();
});
test("restart kernel", async ({ page }) => {
const appUrl = getAppUrl("shutdown.py");
await page.goto(appUrl);
// Wait for page to be fully loaded
await page.waitForLoadState("networkidle");
await page.getByTestId("notebook-menu-dropdown").click();
// Wait for dropdown to be visible and stable
await page.waitForTimeout(100);
const restartButton = page.getByRole("menuitem", { name: "Restart kernel" });
await restartButton.waitFor({ state: "visible" });
await restartButton.click();
const confirmButton = page.getByRole("button", { name: "Confirm Restart" });
await confirmButton.waitFor({ state: "visible" });
await confirmButton.click();
await expect(page.getByText("'None'", { exact: true })).toBeVisible();
});
test("shutdown shows disconnected text", async ({ page }) => {
const appUrl = getAppUrl("shutdown.py");
await page.goto(appUrl);
await page.getByRole("button", { name: "Shutdown" }).click();
// confirm shutdown on modal
await page.getByRole("button", { name: "Confirm Shutdown" }).click();
// kernel disconnected message to be on the page
await expect(page.getByText("kernel not found")).toBeVisible();
// when no unsaved changes, recovery modal should not be shown
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Download unsaved changes?")).toHaveCount(0);
// when changes are made, recovery modal should be shown
await page
.getByRole("textbox")
.filter({ hasText: "import marimo" })
.fill("1234");
await page.getByRole("button", { name: "Save" }).click();
await expect(page.getByText("Download unsaved changes?")).toHaveCount(1);
await takeScreenshot(page, _filename);
});
test.afterAll(() => {
startServer("shutdown.py"); // restart the server
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/stdin.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
const appUrl = getAppUrl("stdin.py");
test("stdin works", async ({ page }) => {
await page.goto(appUrl);
expect(page.getByText("stdin.py")).toBeTruthy();
// Check that "what is your name?" exists in the console
await expect(page.getByTestId("console-output-area")).toHaveText(
"what is your name?",
);
// Expect loading spinner
await expect(page.getByTestId("loading-indicator")).toBeVisible();
// Get input inside the console
const consoleInput = page
.getByTestId("console-output-area")
.getByRole("textbox");
// Type "marimo" into the console
await consoleInput.fill("marimo");
// Hit enter
await consoleInput.press("Enter");
// Check that "Hi, marimo" exists on the page
await expect(page.getByText("Hi marimo")).toBeVisible();
// Expect not loading spinner
await expect(page.getByTestId("loading-indicator")).not.toBeVisible();
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/output.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
import { takeScreenshot } from "./helper";
import { fileURLToPath } from "node:url";
const _filename = fileURLToPath(import.meta.url);
test("it can clear and append output", async ({ page }) => {
const appUrl = getAppUrl("output.py//run");
await page.goto(appUrl);
// Flakey: Test that Loading replaced exists at least once
// await expect(page.getByText("Loading replace")).toBeVisible();
// Now wait for Replaced to be visible
await expect(page.getByText("Replaced!")).toBeVisible();
// Test that Loading replaced does not exist
await expect(page.getByText("Loading replace")).not.toBeVisible();
// Test the end state of the output
await expect(page.getByText("Appended!")).toBeVisible();
await expect(page.getByText("Loading 0/5").first()).toBeVisible();
await expect(page.getByText("Loading 4/5").first()).toBeVisible();
// Test that Cleared does not exist
await expect(page.getByText("Cleared!")).not.toBeVisible();
// Test that Replaced by index is visible and To be replaced is not
await expect(page.getByText("To be replaced.")).not.toBeVisible();
await expect(page.getByText("Replaced by index!")).toBeVisible();
await takeScreenshot(page, _filename);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/streams.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl } from "../playwright.config";
const appUrl = getAppUrl("streams.py");
test("stdout, stderr redirected to browser", async ({ page }) => {
await page.goto(appUrl);
await expect(page).toHaveTitle("streams");
expect(page.getByText("streams.py")).toBeTruthy();
// text printed using Python to be in the document.
await expect(page.getByText(/^Hello, python!/)).toBeVisible();
// text echoed to stdout, stderr to be in the document.
await expect(page.getByText(/^Hello, stdout!/)).toBeVisible();
await expect(page.getByText(/^Hello, stderr!/)).toBeVisible();
});
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/toggle-cell-language.spec.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { test, expect } from "@playwright/test";
import { getAppUrl, resetFile } from "../playwright.config";
import { openCellActions } from "./helper";
const appUrl = getAppUrl("title.py");
test.beforeEach(async ({ page }, info) => {
await page.goto(appUrl);
if (info.retry) {
await page.reload();
}
});
test.afterEach(async () => {
// Need to reset the file because this test modifies it
await resetFile("title.py");
});
test("change the cell to a markdown cell and toggle hide code", async ({
page,
}) => {
const title = page.getByText("Hello Marimo!", { exact: true });
await expect(title).toBeVisible();
// Convert to Markdown
await openCellActions(page, title);
await page.getByText("Convert to Markdown").click();
await expect(title).toBeVisible();
// Verify markdown content
const markdown = page.getByText("import marimo as mo");
await expect(markdown).toBeVisible();
// Hide code
await openCellActions(page, title);
await page.getByText("Hide code").click();
await expect(title).toBeVisible();
// Verify code editor is hidden
const cellEditor = page.getByTestId("cell-editor");
await expect(cellEditor).toBeHidden();
// Unhide code
await openCellActions(page, title);
await page.getByText("Show code").click();
await expect(title).toBeVisible();
// Verify code editor is visible
await expect(cellEditor).toBeVisible();
});
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/__demo__/index.html
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="a marimo app" />
<title>🏝️</title>
<script type="module" src="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/main.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/style.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossorigin
/>
<link href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;family=Lora&amp;family=PT+Sans:wght@400;700&amp;display=swap" rel="stylesheet" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww"
crossorigin="anonymous"
/>
<marimo-filename hidden></marimo-filename>
<marimo-mode data-mode='read' hidden></marimo-mode>
<!-- If running a local server of the production build -->
<!-- <script type="module" src="http://127.0.0.1:8001/main.js"></script>
<link
href="http://127.0.0.1:8001/style.css"
rel="stylesheet"
crossorigin="anonymous"
/> -->
<!-- If running from Vite -->
<!-- <script type="module" src="/src/core/islands/main.ts"></script> -->
</head>
<body>
<marimo-island
data-app-id="main"
data-cell-id="Hbol"
data-reactive="true"
>
<marimo-cell-output>
<span></span>
</marimo-cell-output>
<marimo-cell-code hidden>import%20marimo%20as%20mo</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="MJUe"
data-reactive="true"
>
<marimo-cell-output>
<span class="markdown prose dark:prose-invert"><span class="paragraph">Hello, islands!</span></span>
</marimo-cell-output>
<marimo-cell-code hidden>mo.md('Hello%2C%20islands!')</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="vblA"
data-reactive="true"
>
<marimo-cell-output>
<marimo-ui-element object-id='vblA-0' random-id='eb5f2816-d9cf-4b03-bae6-6c67925d4253'><marimo-slider data-initial-value='0' data-label='null' data-start='0' data-stop='100' data-step='2' data-steps='[]' data-debounce='false' data-orientation='&quot;horizontal&quot;' data-show-value='false' data-full-width='false'></marimo-slider></marimo-ui-element>
</marimo-cell-output>
<marimo-cell-code hidden>%0Aslider%20%3D%20mo.ui.slider(0%2C%20100%2C%202)%0Aslider%0A</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="bkHC"
data-reactive="true"
>
<marimo-cell-output>
<span class="markdown prose dark:prose-invert"><span class="paragraph">Slider value: 0</span></span>
</marimo-cell-output>
<marimo-cell-code hidden>%0Amo.md(f%22Slider%20value%3A%20%7Bslider.value%7D%22)%0A</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="lEQa"
data-reactive="true"
>
<marimo-cell-output>
<span class="markdown prose dark:prose-invert"><span class="paragraph">We can also show the island code!</span></span>
</marimo-cell-output>
<marimo-ui-element object-id='135dcce7-6c7f-49fe-853d-fbcb784342fa' random-id='135dcce7-6c7f-49fe-853d-fbcb784342fa'><marimo-code-editor data-initial-value='&quot;mo.md(&#92;&quot;We can also show the island code!&#92;&quot;)&quot;' data-label='null' data-language='&quot;python&quot;' data-placeholder='&quot;&quot;' data-disabled='false'></marimo-code-editor></marimo-ui-element>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="PKri"
data-reactive="false"
>
<marimo-cell-output>
<marimo-mime-renderer data-mime='&quot;application/vnd.marimo+error&quot;' data-data='[{&quot;msg&quot;: &quot;This cell raised an exception: ModuleNotFoundError(&#x27;No module named &#x27;matplotlib&#x27;&#x27;)&quot;, &quot;exception_type&quot;: &quot;ModuleNotFoundError&quot;, &quot;raising_cell&quot;: null, &quot;type&quot;: &quot;exception&quot;}]'></marimo-mime-renderer>
</marimo-cell-output>
<marimo-ui-element object-id='332c9909-e441-462a-9faa-6a60625e808b' random-id='332c9909-e441-462a-9faa-6a60625e808b'><marimo-code-editor data-initial-value='&quot;# Also run expensive outputs without performing them in the browser&#92;nimport matplotlib.pyplot as plt&#92;nimport numpy as np&#92;nx = np.linspace(0, 2*np.pi, 100)&#92;ny = np.sin(x)&#92;nplt.plot(x, y)&#92;nplt.gca()&quot;' data-label='null' data-language='&quot;python&quot;' data-placeholder='&quot;&quot;' data-disabled='false'></marimo-code-editor></marimo-ui-element>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="Xref"
data-reactive="true"
>
<marimo-cell-output>
<marimo-mime-renderer data-mime='&quot;application/vnd.marimo+error&quot;' data-data='[{&quot;msg&quot;: &quot;This cell raised an exception: ModuleNotFoundError(&#x27;No module named &#x27;idk_package&#x27;&#x27;)&quot;, &quot;exception_type&quot;: &quot;ModuleNotFoundError&quot;, &quot;raising_cell&quot;: null, &quot;type&quot;: &quot;exception&quot;}]'></marimo-mime-renderer>
</marimo-cell-output>
<marimo-cell-code hidden>%0Aimport%20idk_package%0A%22Should%20raise%20an%20error%22%0A</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="SFPL"
data-reactive="true"
>
<marimo-cell-output>
<span class="markdown prose dark:prose-invert"><h1 id="hello-markdown">Hello, Markdown!</h1>
<span class="paragraph">Use marimo's "<code>md</code>" function to embed rich text into your marimo
apps. This function compiles Markdown into HTML that marimo
can display.</span>
<span class="paragraph">For example, here's the code that rendered the above title and
paragraph:</span>
<div class="codehilite"><pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span>
<span class="w"> </span><span class="sd">&#39;&#39;&#39;</span>
<span class="sd"> # Hello, Markdown!</span>
<span class="sd"> Use marimo&#39;s &quot;`md`&quot; function to embed rich text into your marimo</span>
<span class="sd"> apps. This function compiles your Markdown into HTML that marimo</span>
<span class="sd"> can display.</span>
<span class="sd"> &#39;&#39;&#39;</span>
<span class="p">)</span>
</code></pre></div></span>
</marimo-cell-output>
<marimo-cell-code hidden>%0Amo.md(%0A%20%20%20%20%22%22%22%0A%20%20%20%20%23%20Hello%2C%20Markdown!%0A%0A%20%20%20%20Use%20marimo's%20%22%60md%60%22%20function%20to%20embed%20rich%20text%20into%20your%20marimo%0A%20%20%20%20apps.%20This%20function%20compiles%20Markdown%20into%20HTML%20that%20marimo%0A%20%20%20%20can%20display.%0A%0A%20%20%20%20For%20example%2C%20here's%20the%20code%20that%20rendered%20the%20above%20title%20and%0A%20%20%20%20paragraph%3A%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20%20%20%20%20%23%20Hello%2C%20Markdown!%0A%0A%20%20%20%20%20%20%20%20Use%20marimo's%20%22%60md%60%22%20function%20to%20embed%20rich%20text%20into%20your%20marimo%0A%20%20%20%20%20%20%20%20apps.%20This%20function%20compiles%20your%20Markdown%20into%20HTML%20that%20marimo%0A%20%20%20%20%20%20%20%20can%20display.%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20)%0A%20%20%20%20%60%60%60%0A%20%20%20%20%22%22%22%0A)%0A</marimo-cell-code>
</marimo-island>
<marimo-island
data-app-id="main"
data-cell-id="BYtC"
data-reactive="true"
>
<marimo-cell-output>
<span class="markdown prose dark:prose-invert"><h2 id="latex">LaTeX</h2>
<span class="paragraph">You can embed LaTeX in Markdown.</span>
<span class="paragraph">For example,</span>
<div class="codehilite"><pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;$f : \mathbf</span><span class="si">{R}</span><span class="s1"> o \mathbf</span><span class="si">{R}</span><span class="s1">$&#39;</span><span class="p">)</span>
</code></pre></div>
<span class="paragraph">renders <marimo-tex class="arithmatex">||(f : \mathbf{R} o \mathbf{R}||)</marimo-tex>, while</span>
<div class="codehilite"><pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span>
<span class="w"> </span><span class="sa">r</span><span class="sd">&#39;&#39;&#39;</span>
<span class="sd"> \[</span>
<span class="sd"> f: \mathbf{R} o \mathbf{R}</span>
<span class="sd"> \]</span>
<span class="sd"> &#39;&#39;&#39;</span>
<span class="p">)</span>
</code></pre></div>
<span class="paragraph">renders the display math</span>
<marimo-tex class="arithmatex">||[
f: \mathbf{R} o \mathbf{R}.
||]</marimo-tex></span>
</marimo-cell-output>
<marimo-cell-code hidden>%0Amo.md(%0A%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20LaTeX%0A%20%20%20%20You%20can%20embed%20LaTeX%20in%20Markdown.%0A%0A%20%20%20%20For%20example%2C%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(r'%24f%20%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%24')%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20renders%20%24f%20%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%24%2C%20while%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r'''%0A%20%20%20%20%20%20%20%20%5C%5B%0A%20%20%20%20%20%20%20%20f%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%0A%20%20%20%20%20%20%20%20%5C%5D%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20)%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20renders%20the%20display%20math%0A%0A%20%20%20%20%5C%5B%0A%20%20%20%20f%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D.%0A%20%20%20%20%5C%5D%0A%20%20%20%20%22%22%22%0A)%0A</marimo-cell-code>
</marimo-island>
<br />
<br />
<br />
<br />
<hr />
<div class="bg-background p-4 border-2 text-primary font-bold bg-background">
this should not be affected by global tailwind styles
</div>
<div class="marimo">
<div class="bg-background p-4 border-2 text-primary font-bold text-foreground">
this should be affected by global tailwind styles
</div>
</div>
<div class="marimo">
<div class="dark">
<div class="bg-background p-4 border-2 text-primary font-bold text-foreground">
this should be affected by global tailwind styles (dark)
</div>
</div>
</div>
</div>
</body>
</html>
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/__demo__/output.html
```html
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="a marimo app" />
<title>🏝️</title>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/main.js"
></script>
<link
href="https://cdn.jsdelivr.net/npm/@marimo-team/[email protected]/dist/style.css"
rel="stylesheet"
crossorigin="anonymous"
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;700&amp;family=Lora&amp;family=PT+Sans:wght@400;700&amp;display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/[email protected]/dist/katex.min.css"
integrity="sha384-wcIxkf4k558AjM3Yz3BBFQUbk/zgIYC2R0QpeeYb+TwlBVMrlgLqwRjRtGZiK7ww"
crossorigin="anonymous"
/>
<!-- If running a local server of the production build -->
<!-- <script type="module" src="http://127.0.0.1:8001/main.js"></script>
<link
href="http://127.0.0.1:8001/style.css"
rel="stylesheet"
crossorigin="anonymous"
/> -->
</head>
<body>
<marimo-island data-app-id="main" data-cell-id="Hbol" data-reactive="true">
<marimo-cell-output> </marimo-cell-output>
<marimo-cell-code hidden>import%20marimo%20as%20mo</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="MJUe" data-reactive="true">
<marimo-cell-output>
<span class="markdown"
><span class="paragraph">Hello, islands!</span></span
>
</marimo-cell-output>
<marimo-cell-code hidden>mo.md('Hello%2C%20islands!')</marimo-cell-code>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="vblA" data-reactive="true">
<marimo-cell-output>
<marimo-ui-element
object-id="vblA-0"
random-id="e1917f32-0714-42a2-bc58-67add2ba677f"
><marimo-slider
data-initial-value="0"
data-label="null"
data-start="0"
data-stop="100"
data-step="2"
data-steps="[]"
data-debounce="false"
data-orientation='"horizontal"'
data-show-value="false"
data-full-width="false"
></marimo-slider
></marimo-ui-element>
</marimo-cell-output>
<marimo-cell-code hidden
>%0Aslider%20%3D%20mo.ui.slider(0%2C%20100%2C%202)%0Aslider%0A</marimo-cell-code
>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="bkHC" data-reactive="true">
<marimo-cell-output>
<span class="markdown"
><span class="paragraph"
>We can also show the island code!</span
></span
>
</marimo-cell-output>
<marimo-ui-element
object-id="65eb81a8-c2be-4a82-9607-f447a5b76466"
random-id="65eb81a8-c2be-4a82-9607-f447a5b76466"
><marimo-code-editor
data-initial-value='"&#92;nmo.md(&#92;"We can also show the island code!&#92;")&#92;n"'
data-label="null"
data-language='"python"'
data-placeholder='""'
data-disabled="true"
></marimo-code-editor
></marimo-ui-element>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="lEQa" data-reactive="false">
<marimo-cell-output>
<img
src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjguNCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8fJSN1AAAACXBIWXMAAA9hAAAPYQGoP6dpAABciUlEQVR4nO3deVxU9f4/8NfMADOAMIDsyuqGKygq4tImiWalXe9NS3PJ5WZqmbbI/d603fbbrSzLVLQytUUzK9JwK0VRFBV3FATZEZlhHWDm/P4Ap8vPDRX4zMx5PR+P8/h+G84cXkN058VnznkfhSRJEoiIiIhINpSiAxARERFR62IBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZFkAiIiIimWEBJCIiIpIZO9EBrJnJZEJubi5cXFygUChExyEiIqImkCQJZWVl8Pf3h1Ipz7UwFsDbkJubi4CAANExiIiI6BZkZ2ejffv2omMIwQJ4G1xcXADU/wK5uroKTkNERERNodfrERAQYH4flyMWwNtw+WNfV1dXFkAiIiIrI+fTt+T5wTcRERGRjLEAEhEREckMCyARERGRzLAAEhEREckMCyARERGRzLAAEhEREckMCyARERGRzLAAEhEREckMCyARERGRzFhFAdy1axceeOAB+Pv7Q6FQYOPGjTd8zo4dO9CnTx+o1Wp07NgR8fHxV+yzZMkSBAcHQ6PRICoqCsnJyc0fnoiIiMjCWEUBrKioQHh4OJYsWdKk/TMyMjBy5EjcfffdSE1Nxdy5czFt2jT89ttv5n3WrVuHefPmYdGiRTh48CDCw8MRGxuLwsLClnoZRERERBZBIUmSJDrEzVAoFNiwYQNGjx59zX1eeOEF/Pzzz0hLSzM/Nm7cOJSWliIhIQEAEBUVhX79+uHjjz8GAJhMJgQEBGDOnDlYsGBBk7Lo9XpotVrodDreC5iIiMhK8P0bsBMdoCUkJSUhJiam0WOxsbGYO3cuAKCmpgYpKSmIi4szf12pVCImJgZJSUnXPK7BYIDBYDD/s16vb97gZJX01bU4kavHyfwylFTUoLrWiOpaI6pqjTCaAC8XNfzdNPB11cDfzRHBns5oo7bJ//SIiMhK2OS7UH5+Pnx8fBo95uPjA71ej6qqKly6dAlGo/Gq+5w8efKax128eDFefvnlFslM1qNQX41f0/KxO70Yx/P0uHCp6qaer1IqEN5ei0EdPRHdoS36BLpDY69qobRERERXsskC2FLi4uIwb9488z/r9XoEBAQITESt5XLp+/loHvZnluD/P3GinZsjuvq5wFergcZOBUcHFTT2KigUQKHegDxdFfJ11cgprUZxuQEHs0pxMKsUH21Lh9pOiRE9fDFhQBAig9yhUCjEvEgiIpINmyyAvr6+KCgoaPRYQUEBXF1d4ejoCJVKBZVKddV9fH19r3lctVoNtVrdIpnJMqUXluGTHWfxY2oujKa/Wl/vQDfEdvdFeHs3dPVzgZuTQ5OPeeFSJfacvYg96cXYc/YiCssM2Jiai42pueji44LxAwIxunc7uGrsW+IlERER2WYBjI6Oxi+//NLosa1btyI6OhoA4ODggMjISCQmJpovJjGZTEhMTMTs2bNbOy5ZoLQcHZZsT0fCsXzzal9EgBvu7+WHET390M7N8ZaP3d7dCQ/3dcLDfQMgSRKOXNBhzb4s/Hg4B6cKyrDwx2N4J+EUnry7I6YMCubHw0RE1OysogCWl5cjPT3d/M8ZGRlITU2Fh4cHAgMDERcXh5ycHKxevRoA8MQTT+Djjz/G888/j8cffxzbtm3D+vXr8fPPP5uPMW/ePEyaNAl9+/ZF//798cEHH6CiogJTpkxp9ddHliO3tAqv/HQcCcfyzY8N6+aDWXd3RHiAW7N/P4VCgfAAN4QHuOFfI7ti46EcfLn3PNILy/FWwkl8mZSJ54Z3wajwdlAq+dEwERE1D6sYA7Njxw7cfffdVzw+adIkxMfHY/LkycjMzMSOHTsaPeeZZ57B8ePH0b59e7z44ouYPHlyo+d//PHHeOedd5Cfn4+IiAh8+OGHiIqKanIuXkZuO+qMJqxKOo/3t5xCRY0RSgXwQLg/nryrI7r4urRqFpNJwoZDOXh3yynk6aoBAD3aueLlB3sgMsi9VbMQEdkivn9bSQG0VPwFsg1HL+gQt+EI0nLqx/pEBrnj9Yd6IMxX7L/T6lojVuzOwKfbz6LMUAelAnjyro54amgnONhZxQx3IiKLxPdvFsDbwl8g62YySfh4ezo++P00TBLgqrFD3H1dMbZvgEV93Hqx3IDXfj6BDYdyANSvBn4wNgIdvVt3ZZKIyFbw/ZsF8LbwF8h6lVTUYO66VOw6XQQAeDDcHy/e3w1eLpZ7lffPR/LwfxuPorSyFmo7JRaMCMPkgcEcG0NEdJP4/s0CeFv4C2SdDmVdwqyvDyJXVw2NvRKvje6Jv0e2Fx2rSQr01XjuuyPm4jo6wh9vjunFK4WJiG4C378BnkhEsvJlUiYe/iwJubpqhHg6Y8OTg6ym/AGAj6sGq6b0w6IHukGlVGBjai4eXbYXRWWGGz+ZiIioAQsgyYIkSXjz15N48cdjqDVKuK+nLzbNHoSuftb3l59CocCUQSFYNaU/XDV2OJhVitFLduN4Lu9NTURETcMCSDav1mjCs98ewdKdZwEAz8V2wZJH+8DFyu+0MbiTJzbOGoRQT2fklFbh70v3IPFEwY2fSEREsscCSDatsqYOM1YfwPcHL0ClVODtv/fCrLs72syFE6FebbDhyUEY1LEtKmuM+OeXKfj5SJ7oWEREZOFYAMlmXaqowaPL9mH7qSJo7JX4/LFIPNw3QHSsZqd1skf8lP54qHc71JkkzPnmIDY2jIwhIiK6Gqu4FRzRzdJV1eKxFfuQlqOH1tEeKyb3s+m7aNirlHj3H+GwUyrwbcoFPLM+FTVGk00WXiIiun0sgGRzyg11mLwyGWk5erR1dsA3Mwags4/tD01WKRV4a0wvONgp8fW+LDz/3RHUGk0YHxUkOhoREVkYfgRMNqWqxohpq/bjUFYptI72+HJqlCzK32VKpQKvje6ByQODAQD/tyEN6/dniw1FREQWhwWQbIahzoh/fpWCvedK4KK2w+rH+6Obv/WNebldCoUCix7ohmmDQwAAcRuO4vfjvDqYiIj+wgJINqHOaMKcNYew63QRHO1VWDmlH8ID3ETHEkahUOD/RnbFmD7tYTRJmLXmIFLOl4iORUREFoIFkGzCaz+fwJbjBXCwU2L5pL7oG+whOpJwCoUCb47piXvCvGGoM+Hx+AM4U1AmOhYREVkAFkCyevG7MxC/JxMA8N+xERjY0VNsIAtir1JiyaN90DvQDbqqWkxckYzc0irRsYiISDAWQLJq204W4JXNxwEAC0aEYURPP8GJLI+jgworJvVDBy9n5OmqMWXlfpQb6kTHIiIigVgAyWody9Vh9ppDMEnA2L4B+OcdoaIjWSx3ZwesnhoFbxc1ThWUYf76VJhMkuhYREQkCAsgWaUCfTWmxh9AZY0Rgzq2xWsP9bCZ27u1lHZujlj6WCQcVEr8dqwAH21LFx2JiIgEYQEkq1NTZ8I/v0xBvr4aHbyc8cn4SNir+KvcFH0C3fHaQz0AAP/5/TR+O5YvOBEREYnAd02yOm/8cgKp2aVw1dhhxeR+0Drai45kVR7uG2AeFD1vXSpO88pgIiLZYQEkq7LpcK75it//jI1AUFtnsYGs1P+N7Iro0LaoqDFi+uoD0FXWio5EREStiAWQrMaZgjIs+P4IAGDW3R0wtKuP4ETWy16lxJLxfdDOzRHnL1biue8OQ5J4UQgRkVywAJJVKDfU4YmvUlBZY8TADm0x794uoiNZPQ9nB3zWcFHIluMFWJ10XnQkIiJqJSyAZPEkScKC74/gbFEFfFzV+PCR3lApecVvc+jRTou4+8IAAK//fAJpOTrBiYiIqDWwAJLFW7c/G5uP5MFOqcAn4/vAs41adCSbMnlgMGK6+qDGaMKcbw5xSDQRkQywAJJFyyiuwMs/1d/p49nYLogM4j1+m5tCocA7f+8FP60GGcUVWLgxTXQkIiJqYSyAZLFqjSbMXZeKqlojokPbYsYQ3umjpbg7O+DDR3pDqQB+OJSD71IuiI5EREQtiAWQLNZH29JxuGHe33sPh0PJ8/5aVL9gDzwT0xkAsPDHNGRdrBSciIiIWgoLIFmklPMl+HjbGQDA6w/1hL+bo+BE8vDk3R3RP8QDlTVGPPvdYd4vmIjIRrEAksUpN9Rh7rpUmCTgb73b4YFwf9GRZEOlVODdv4fDyUGF5IwSrGwYuk1ERLaFBZAszqs/HUd2SRXauTnipVHdRceRncC2Tvi/kV0BAG8nnMTZonLBiYiIqLlZVQFcsmQJgoODodFoEBUVheTk5Gvue9ddd0GhUFyxjRw50rzP5MmTr/j68OHDW+Ol0DXsOl2EdQeyoVDU3+rNVcP7/IrwaP9ADOnkCUOdCfPXH0ad0SQ6EhERNSOrKYDr1q3DvHnzsGjRIhw8eBDh4eGIjY1FYWHhVff/4YcfkJeXZ97S0tKgUqnwj3/8o9F+w4cPb7TfN9980xovh66iwlCHuB+OAgAmRQejfwhHvoiiUCjw1phecNHYITW7FJ//cU50JCIiakZWUwDff/99TJ8+HVOmTEG3bt2wdOlSODk5YcWKFVfd38PDA76+vuZt69atcHJyuqIAqtXqRvu5u7u3xsuhq3g74SRySqvQ3t0Rz8XyVm+i+bs5YtED9R/B/2fraZzM1wtOREREzcUqCmBNTQ1SUlIQExNjfkypVCImJgZJSUlNOsby5csxbtw4ODs7N3p8x44d8Pb2RpcuXTBz5kxcvHjxmscwGAzQ6/WNNmoe+zNLsKrhXrRv/q0XnNV2ghMRAIzp0w4xXX1Qa5TwwvdHYeRVwURENsEqCmBxcTGMRiN8fHwaPe7j44P8/PwbPj85ORlpaWmYNm1ao8eHDx+O1atXIzExEW+99RZ27tyJESNGwGg0XvU4ixcvhlarNW8BAQG3/qLIrLrWiBe+OwIAGNs3AIM7eQpORJcpFAq8/lAPuKjtcDi7FF/tPS86EhERNQOrKIC3a/ny5ejZsyf69+/f6PFx48bhwQcfRM+ePTF69Ghs3rwZ+/fvx44dO656nLi4OOh0OvOWnZ3dCult3we/n8G54gp4u6jxr4arT8ly+Lhq8PyIMAD1H9Pn6aoEJyIiottlFQXQ09MTKpUKBQUFjR4vKCiAr6/vdZ9bUVGBtWvXYurUqTf8PqGhofD09ER6evpVv65Wq+Hq6tpoo9uTlqPDsoYLDF4b3QNaR171a4nG9w9En0A3VNQYsejHY6LjEBHRbbKKAujg4IDIyEgkJiaaHzOZTEhMTER0dPR1n/vtt9/CYDBgwoQJN/w+Fy5cwMWLF+Hn53fbmenGTCYJ/96YBqNJwshefhjW/fplnsRRKhVY/LdesFMqsOV4AX47duNTL4iIyHJZRQEEgHnz5mHZsmVYtWoVTpw4gZkzZ6KiogJTpkwBAEycOBFxcXFXPG/58uUYPXo02rZt2+jx8vJyPPfcc9i7dy8yMzORmJiIUaNGoWPHjoiNjW2V1yR36w5kIzW7FG3Udlh4fzfRcegGuvi6YMYdoQCART8eQ1l1reBERER0q6zmUsuxY8eiqKgICxcuRH5+PiIiIpCQkGC+MCQrKwtKZeM+e+rUKfz555/YsmXLFcdTqVQ4cuQIVq1ahdLSUvj7+2PYsGF49dVXoVarW+U1ydnFcgPe/PUkAGDevZ3h46oRnIia4qmhnfDz0Tycv1iJd387hZdH9RAdiYiIboFCkiTOdbhFer0eWq0WOp2O5wPepOe/O4z1By6gq58rfpo9CHYqq1mMlr0/zxRjwvJ9UCiATbMGo2d7rehIREQ3he/fVvQRMNmOA5klWH/gAoD6Cz9Y/qzL4E6eeDDcH5IELNqUBhNnAxIRWR2+81KrqjOa8O+NaQCAcf0CEBnEO69Yo3/d1xVODioczCrFhkM5ouMQEdFNYgGkVhW/JxMn88vg7mSPF4aHiY5Dt8hXq8GcezoBABb/epIXhBARWRkWQGo1RWUGfPD7GQDAC8PD4O7sIDgR3Y7HBwcjxNMZxeUGfJh4RnQcIiK6CSyA1Gre33oK5YY69GqvxcN9eRs9a6e2U2HhA/Xje1buzkR6YZngRERE1FQsgNQqjuXqsHZ//a3zFt7fDUqlQnAiag53d/FGTFdv1JkkvPzTcXCoABGRdWABpBYnSRJe+ek4JAl4INwffYM9REeiZvTi/d3gYKfEH2eK8duxghs/gYiIhGMBpBb327F87MsogdpOiQUjeOGHrQlq64x/Ntwh5I1fTqCmziQ4ERER3QgLILWo6lojXv/lBADgn3eEop2bo+BE1BJm3tUB3i5qZJVUYnVSpug4RER0AyyA1KJW7M5AdkkVfF01eOKuDqLjUAtxcrDD/GGdAQAfbUtHaWWN4ERERHQ9LIDUYgrLqrFkWzoA4IURXeDkYDW3nqZb8PfIAIT5ukBXVYsPE9NFxyEioutgAaQW85+tZ1BRY0REgBtGhbcTHYdamEqpwP+N7AoA+HJvJjKLKwQnIiKia2EBpBaRXliO9Qfqx778e2RXjn2RiSGdvHBXFy/UGiW8lXBSdBwiIroGFkBqEW8nnITRJOHebj4c+yIz/7qvK5QK4Ne0fOzPLBEdh4iIroIFkJrdgcwSbDleAKUCeGF4F9FxqJV19nHB2H6BAIDXfj4Bk4nDoYmILA0LIDUrSZKw+Nf6j/7G9gtAR28XwYlIhGfu7QRnBxUOZ5fil7Q80XGIiOj/wwJIzeq3YwVIOX8JGnsl5sZ0Fh2HBPF20WB6w3Do97acRq2Rw6GJiCwJCyA1mzqjCW//Vr/6N21wKHxcNYITkUjThoTCw9kBGcUV+C7lgug4RET0P1gAqdmsO5CNc0UV8HB2wD/vDBUdhwRro7bD7Ls7AgA++P00qmuNghMREdFlLIDULKpqjPjg9zMAgKfu6QgXjb3gRGQJxg8IRDs3RxToDVi1J1N0HCIiasACSM1iVVImisoMaO/uiEejgkTHIQuhtlPhmXvrzwX9ZMdZ6KpqBSciIiKABZCaQVl1LZbuPAsAmBvTGQ52/LWivzzUux06ebeBrqoWn+86KzoOERGBBZCawfI/M1BaWYsOXs54qDdv+UaNqZQKPBtbPw9yxZ+ZKCyrFpyIiIhYAOm2XKqowRd/ZAAA5t3bBSre8o2uYlg3H0QEuKGq1oiPEtNFxyEikj0WQLotn+06h3JDHbr6uWJED1/RcchCKRQKPN9wV5i1+7OQU1olOBERkbyxANItKyyrRvye+tW/Z4d1hpKrf3QdAzt4Ijq0LWqNEj7exlVAIiKRWADpln2y/Syqa03oHeiGe8K8RcchK3D5iuBvD2Qju6RScBoiIvliAaRbklNahTX7sgAAzw3rAoWCq390Y/1DPDCkkyfqTBI+2nZGdBwiItliAaRb8vG2dNQYTRjYoS0GdvQUHYesyOV7RH9/MAeZxRWC0xARyRMLIN20C5cq8e2BbAB/faRH1FSRQe64q4sXjCYJH3IVkIhICBZAummf7DiLOpOEQR3bol+wh+g4ZIWeaVgF3HgoB2eLygWnISKSH6sqgEuWLEFwcDA0Gg2ioqKQnJx8zX3j4+OhUCgabRqNptE+kiRh4cKF8PPzg6OjI2JiYnDmDFckrientMq8+vf0UK7+0a0JD3BDTFdvmCTgw0T+N0dE1NqspgCuW7cO8+bNw6JFi3Dw4EGEh4cjNjYWhYWF13yOq6sr8vLyzNv58+cbff3tt9/Ghx9+iKVLl2Lfvn1wdnZGbGwsqqt5p4Jr+WR7OmqNEgZ2aIv+IVz9o1t3+VzATYdzcaagTHAaIiJ5sZoC+P7772P69OmYMmUKunXrhqVLl8LJyQkrVqy45nMUCgV8fX3Nm4+Pj/lrkiThgw8+wL///W+MGjUKvXr1wurVq5Gbm4uNGze2wiuyPjmlVVhvXv3rJDgNWbse7bSI7e4DSQI+3s65gERErckqCmBNTQ1SUlIQExNjfkypVCImJgZJSUnXfF55eTmCgoIQEBCAUaNG4dixY+avZWRkID8/v9ExtVotoqKirnlMg8EAvV7faJOTT3fUr/5Fh7ZFVGhb0XHIBsy5p/4PiZ8O5+IczwUkImo1VlEAi4uLYTQaG63gAYCPjw/y8/Ov+pwuXbpgxYoV+PHHH/HVV1/BZDJh4MCBuHDhAgCYn3czx1y8eDG0Wq15CwgIuN2XZjVyS6uwbn/D6l8MV/+oefRop8XQsPpzAZdsPys6DhGRbFhFAbwV0dHRmDhxIiIiInDnnXfihx9+gJeXFz777LNbPmZcXBx0Op15y87ObsbElu3THWdRa5QwINQDA7j6R81oTsPpBBtTc5B1kXcHISJqDVZRAD09PaFSqVBQUNDo8YKCAvj6+jbpGPb29ujduzfS0+vPNbr8vJs5plqthqura6NNDvJ11X+t/vHKX2pmEQFuGNLJE0aThE938lxAIqLWYBUF0MHBAZGRkUhMTDQ/ZjKZkJiYiOjo6CYdw2g04ujRo/Dz8wMAhISEwNfXt9Ex9Xo99u3b1+RjysXnu86hxmhC/2APRHfg6h81v6caVgG/S7mAnNIqwWmIiGyfVRRAAJg3bx6WLVuGVatW4cSJE5g5cyYqKiowZcoUAMDEiRMRFxdn3v+VV17Bli1bcO7cORw8eBATJkzA+fPnMW3aNAD1VwjPnTsXr732GjZt2oSjR49i4sSJ8Pf3x+jRo0W8RIt0sdyANcn143Nm3dNRcBqyVf2CPTAg1AO1Rgmf7eS5gERELc1OdICmGjt2LIqKirBw4ULk5+cjIiICCQkJ5os4srKyoFT+1WcvXbqE6dOnIz8/H+7u7oiMjMSePXvQrVs38z7PP/88KioqMGPGDJSWlmLw4MFISEi4YmC0nK3YnYHqWhN6ttPijk685y+1nKeGdsLec/uwdn82Zt/dEd6u/O+QiKilKCRJkkSHsFZ6vR5arRY6nc4mzwfUVdVi8JvbUGaow9IJkRjeo2nnWxLdCkmS8I+lSThw/hKmDg7Bi/d3u/GTiIhuga2/fzeF1XwETK3vy6RMlBnq0NmnDYZ187nxE4hug0KhMF8RvGZfFkoqagQnIiKyXSyAdFWVNXVY/mcGAODJuzpCqVQITkRycEcnT/Rsp0VVrRHxuzNExyEislksgHRVa/Zl4VJlLQI9nHB/Lz/RcUgmFAoFnryrAwAgfk8myg11ghMREdkmFkC6gqHOiGV/nAMAzLyrA+xU/DWh1hPb3RehXs7QV9dhzb7zouMQEdkkvrPTFb5PyUGB3gBfVw3+1qed6DgkM0qlAk/cWb8KuOyPDFTXGgUnIiKyPSyA1IjRJOGzXfVz2KbfEQq1nUpwIpKj0RHt4KfVoKjMgO8PXhAdh4jI5rAAUiO/puXh/MVKuDnZ45H+AaLjkEw52CkxfUgoAOCznedQZzQJTkREZFtYAMlMkiR8uqN+9W9SdDCcHKxmTjjZoHH9A+DuZI+skkr8fDRPdBwiIpvCAkhmf6YX41iuHo72KkwaGCw6Dsmck4MdpgwKAQB8uuMsOLOeiKj5sACS2eXVv7H9AuDh7CA4DVH9SrSzgwon88uw7WSh6DhERDaDBZAAAIezS7Hn7EXYKRWYNiREdBwiAIDWyR7jBwQBAD7bdU5wGiIi28ECSACApTvrV/8eDPdHe3cnwWmI/jJlUDDsVQokZ5TgYNYl0XGIiGwCCyDhXFE5Eo7lAwD+2TB/jchS+GkdMSqifh7l5zu5CkhE1BxYAAnL/jgHSQKGhnmji6+L6DhEV5hxR/1ImN+O5+NcUbngNERE1o8FUOYK9dX4PiUHAPDEXVz9I8vU2ccFQ8O8IUnAF39miI5DRGT1WABlLn5PJmqMJkQGuaNfsIfoOETXdHkV8LuUCygqMwhOQ0Rk3VgAZazcUIev9p4H8NebK5Gl6h/igYgAN9TUmbBqT6boOEREVo0FUMbW78+GvroOIZ7OiOnqIzoO0XUpFAo8cWf9Hypf7j2PCkOd4ERERNaLBVCm6owmLG84l2rakBColArBiYhu7N5uvgjxdIauqhbr9meLjkNEZLVYAGXql7R85JRWwcPZAWP6tBcdh6hJVEoFpg+pXwVc/mcGao0mwYmIiKwTC6AMSZKEz3fVD36eGB0Ejb1KcCKipvtbn3bwbOOAnNIq/HI0T3QcIiKrxAIoQ0nnLiItRw+1nRITo4NFxyG6KRp7FR4bEAwA+OKPDEiSJDYQEZEVYgGUoWUN91T9R9/28HB2EJyG6OZNGBAItZ0SR3N02JdRIjoOEZHVYQGUmdMFZdh+qggKBTB1MEe/kHVq20aNMZH1565+8QdvD0dEdLNYAGXm8upfbMPVlETWaurgEADA7ycKcZa3hyMiuiksgDJSWFaNH1NzAQDT7wgRnIbo9nTwaoOYrt4AYB5pRERETcMCKCNf7c1CjdGE3oFuiAzibd/I+k1rGAnzfcoFXCzn7eGIiJqKBVAmqmuN5tu+TeO5f2QjokI80LOdFoY6E77amyU6DhGR1WABlIkNh3JQUlGDdm6OiO3O276RbVAoFJg2pP50htVJmaiuNQpORERkHVgAZcBkksznSE0ZFAw7Ff+1k+24r6cf/LUaXKyowYZDOaLjEBFZBTYBGdh5pgjpheVoo7bD2H4BouMQNSt7lRJTBtWvAq74k4OhiYiawqoK4JIlSxAcHAyNRoOoqCgkJydfc99ly5ZhyJAhcHd3h7u7O2JiYq7Yf/LkyVAoFI224cOHt/TLaHXL/6hf/RvXLwAuGnvBaYia39j+AXB2UOFMYTl2nSkWHYeIyOJZTQFct24d5s2bh0WLFuHgwYMIDw9HbGwsCgsLr7r/jh078Mgjj2D79u1ISkpCQEAAhg0bhpycxh8RDR8+HHl5eebtm2++aY2X02pO5OnxZ3oxlApg8qBg0XGIWoSrxh7/6Fu/us2RMEREN2Y1BfD999/H9OnTMWXKFHTr1g1Lly6Fk5MTVqxYcdX9v/76azz55JOIiIhAWFgYvvjiC5hMJiQmJjbaT61Ww9fX17y5u7u3xstpNZffDEf09EN7dyfBaYhazuODQqBQALtOF+FMQZnoOEREFs0qCmBNTQ1SUlIQExNjfkypVCImJgZJSUlNOkZlZSVqa2vh4dF4/t2OHTvg7e2NLl26YObMmbh48eI1j2EwGKDX6xttlqywrBqbGgY/TxvMwc9k2wLbOmFYt/or3Ffs5iogEdH1WEUBLC4uhtFohI9P4/ElPj4+yM/Pb9IxXnjhBfj7+zcqkcOHD8fq1auRmJiIt956Czt37sSIESNgNF59lMTixYuh1WrNW0CAZV9QcXnwc59AN/QOtK2VTaKruXx/6x8O1o89IiKiq7OKAni73nzzTaxduxYbNmyARqMxPz5u3Dg8+OCD6NmzJ0aPHo3Nmzdj//792LFjx1WPExcXB51OZ96ys7Nb6RXcvOpaI75uGPw8lYOfSSb6BbubB0Nf/v0nIqIrWUUB9PT0hEqlQkFBQaPHCwoK4Ovre93nvvvuu3jzzTexZcsW9OrV67r7hoaGwtPTE+np6Vf9ulqthqura6PNUm06nIuLFTXw12o4+JlkQ6FQYGrD6Q6r956HoY6DoYmIrsYqCqCDgwMiIyMbXcBx+YKO6Ojoaz7v7bffxquvvoqEhAT07dv3ht/nwoULuHjxIvz8/JoltyiSJGFFw8UfkwZy8DPJy309/eDjqkZRmQGbD+eJjkNEZJGsphnMmzcPy5Ytw6pVq3DixAnMnDkTFRUVmDJlCgBg4sSJiIuLM+//1ltv4cUXX8SKFSsQHByM/Px85Ofno7y8HABQXl6O5557Dnv37kVmZiYSExMxatQodOzYEbGxsUJeY3NJOnsRJ/PL4OSgwrh+gaLjELUqBzslJkYHA6i/Cp6DoYmIrmQ1BXDs2LF49913sXDhQkRERCA1NRUJCQnmC0OysrKQl/fXX/uffvopampq8Pe//x1+fn7m7d133wUAqFQqHDlyBA8++CA6d+6MqVOnIjIyEn/88QfUarWQ19hcLl8B+ffI9tA6cfAzyc/4qEBo7JU4nqdHckaJ6DhERBZHIfHP41um1+uh1Wqh0+ks5nzAjOIK3PPeDkgSsG3+nQj1aiM6EpEQ/9pwFGv2ZSG2uw8+e+zGp4AQkXxY4vt3a7OaFUBqmlV7MiFJwD1h3ix/JGtTBgYDALYeL0B2SaXYMEREFoYF0Iboqmqx/kD9aJrHB3HwM8lbJx8XDOnkCZNU/4cRERH9hQXQhnx7IBuVNUZ08XHBoI5tRcchEu7yH0LrDmSj3FAnOA0RkeVgAbQRRpOE+IZVjscHB0OhUIgNRGQB7uzshVBPZ5RV1+H7lAui4xARWQwWQBux9XgBLlyqgruTPUZFtBMdh8giKJUKTB4UDACI35MJk4nXvBERASyANmNlw+iXR/oHQmOvEpyGyHKM6dMeLho7ZBRXYOfpItFxiIgsAgugDTiWq8O+jBKolAo8Fh0kOg6RRXFW22Fs3wAAf83IJCKSOxZAG3D5CscRPXzhp3UUG4bIAk0aGAylAvjjTDFOF5SJjkNEJBwLoJW7WG7AxtRcAMCUhnOdiKixAA8n3Nut/q5BK3dnig1DRGQBWACt3DfJWaipM6FXey36BLqLjkNksaY0jITZcOgCSitrBKchIhKLBdCK1RpN+HLveQD1q38c/UJ0bVEhHgjzdUF1rQnr9meLjkNEJBQLoBX7NS0fBXoDPNuocV9PP9FxiCyaQqEwD4ZenXQedUaT4EREROKwAFqxy6NfJgwIhNqOo1+IbuTBCH+4O9kjp7QKv58oFB2HiEgYFkArlZpdikNZpbBXKTA+iqNfiJpCY6/CI/0DAQDxezgShojkiwXQSl0e/fJAL394uajFhiGyIo9FB0GlVGDvuRKcyNOLjkNEJAQLoBUqLKvG5iOXR7+ECE5DZF38tI4Y3sMXABDPkTBEJFMsgFZozb4s1BolRAa5o2d7reg4RFZnysBgAMDG1ByUVHAkDBHJDwuglampM+GrvVkA6u9uQEQ3LzLIHT3aucJQZ8La/Vmi4xARtToWQCvzy9E8FJcb4OOqxoiGj7GI6OYoFApMGVh/+sSXHAlDRDLEAmhlVjZc/DEhKgj2Kv7rI7pV94f7wbONA/J01fjtWIHoOERErYoNwoocyrqEw9mlcFAp8UhUoOg4RFZNbafCow0jYS5fVU9EJBcsgFYk/vLol3B/eLbh6Bei2zV+QBDslAokZ5bgWK5OdBwiolbDAmglCvTV+PlIHgBgMi/+IGoWPq4ajGi4jSJXAYlITlgArcTX+7JQZ5LQl6NfiJrV5IH1d9LZmJrLkTBEJBssgFbAUGfEmn3nAQCTBwWLDUNkY/oEuqNnOy1qOBKGiGSEBdAK/HwkD8XlNfBxVSO2O0e/EDUnhUJhPq3iK46EISKZYAG0ApfPTXpsAEe/ELWE+8P90NbZAbm6amw9zpEwRGT72CYs3KGsSzh8QQcHlRLj+nP0C1FLUNup8GjDaKWVvBiEiGSABdDCreLoF6JWMT6qYSRMRgmO5+pFxyEialEsgBassKwaPx/l6Bei1uCr1WB4w+0V4/dkCE5DRNSyWAAt2Jp9Wag1SugT6MbRL0St4PIfWj+m5uISR8IQkQ2zqgK4ZMkSBAcHQ6PRICoqCsnJydfd/9tvv0VYWBg0Gg169uyJX375pdHXJUnCwoUL4efnB0dHR8TExODMmTMt+RKarKbOhK/31Y+kmDwoRHAaInmIDHJHj3auMNSZsHZ/tug4REQtxmoK4Lp16zBv3jwsWrQIBw8eRHh4OGJjY1FYWHjV/ffs2YNHHnkEU6dOxaFDhzB69GiMHj0aaWlp5n3efvttfPjhh1i6dCn27dsHZ2dnxMbGorq6urVe1jX9mpaHojIDvF3UGNGDo1+IWoNCocCk6GAAwFd7ORKGiGyXQpIkSXSIpoiKikK/fv3w8ccfAwBMJhMCAgIwZ84cLFiw4Ir9x44di4qKCmzevNn82IABAxAREYGlS5dCkiT4+/tj/vz5ePbZZwEAOp0OPj4+iI+Px7hx426YSa/XQ6vVQqfTwdXVtZleab2HPtmNQ1mlmHdvZzw1tFOzHpuIrq261oiBb25DSUUNlk7og+E9/ERHIqJm1pLv39bCKlYAa2pqkJKSgpiYGPNjSqUSMTExSEpKuupzkpKSGu0PALGxseb9MzIykJ+f32gfrVaLqKioax7TYDBAr9c32lrC4exSHMoqhYNKiUc4+oWoVWnsVXikfwAAIJ4jYYiE2XayAJNWJGN3erHoKDbJKgpgcXExjEYjfHx8Gj3u4+OD/Pz8qz4nPz//uvtf/r83c8zFixdDq9Wat4CAgFt6PTeyKikTAHB/Lz94uXD0C1FrmzAgCCqlAnvPleBEHkfCEImwcncmdp4uws7TRaKj2CSrKICWIi4uDjqdzrxlZ7fMSeLPx4Zhzj0d8fhgXvxBJIKf1hHDG267uLrhDzIiaj3pheX440wxFIr6u2BR87OKAujp6QmVSoWCgsa3aCooKICv79UvkPD19b3u/pf/780cU61Ww9XVtdHWEny1Gswf1gU92nH0C5EokxpGwmw4lIPSSo6EIWpNl//wiunqgwAPJ7FhbJRVFEAHBwdERkYiMTHR/JjJZEJiYiKio6Ov+pzo6OhG+wPA1q1bzfuHhITA19e30T56vR779u275jGJSD76Bbujq58rqmtNWMeRMEStRl9di+9SLgDgTRBaklUUQACYN28eli1bhlWrVuHEiROYOXMmKioqMGXKFADAxIkTERcXZ97/6aefRkJCAt577z2cPHkSL730Eg4cOIDZs2cDqB/3MHfuXLz22mvYtGkTjh49iokTJ8Lf3x+jR48W8RKJyIIoFApMaXjz+XLveRhNVjEwgcjqfXfgAiprjOjk3QYDO7QVHcdm2YkO0FRjx45FUVERFi5ciPz8fERERCAhIcF8EUdWVhaUyr/67MCBA7FmzRr8+9//xr/+9S906tQJGzduRI8ePcz7PP/886ioqMCMGTNQWlqKwYMHIyEhARqNptVfHxFZngcj/LH41xO4cKkKv58oQGx3zuQkakkmk2T++HfSwGAoFAqxgWyY1cwBtEScI0Rk+95KOIlPd5zFwA5tsWb6ANFxiGza9pOFmBK/Hy4aO+yNGwpndcusU/H924o+AiYiEmHCgCAoFcCesxdxKr9MdBwim7ayYfbm2L4BLVb+qB4LIBHRdbRzczR/9LuKI2GIWszZonLsOl0EhQKY2HBLRmo5LIBERDdgHglzMAe6ylqxYYhs1OqG1b97ungjsC1Hv7Q0FkAiohuICvFAmK8LqmqNWH+AI2GImlvZ/45+GRQsNoxMsAASEd2AQqHAlIY3pVVJmRwJQ9TMvku5gIoaIzp4OWNwR0/RcWSBBZCIqAlGRbSDm5M9LlyqwraThaLjENmM+tEv5wHUD37m6JfWwQJIRNQEGnsVxvULBADE78kQnIbIduw8U4SM4gq4qO3wtz7tRceRDRZAIqImmjAgEEoFsDv9Ik4XcCQMUXOI350JAHi4H0e/tCYWQCKiJmrv7oRh3epHwsQ3XLFIRLfubFE5dppHvwSJjiMrLIBERDfh8hWKHAlDdPsuj34ZGuaNoLbOYsPIDAsgEdFN4EgYouah/9/RLwNDBKeRHxZAIqKbwJEwRM3juwP1o186erfBoI5tRceRHRZAIqKb9L8jYX4/USA6DpHVqR/9kgmAo19EYQEkIrpJGnsVHunfMBKm4QpGImq6naeLkHmxEi4aO/ytTzvRcWSJBZCI6BY8NiAIKqUCSecu4mS+XnQcIquyYnf9LM2xfQPg5MDRLyKwABIR3QJ/N0cM794wEoargERNll5Yhj/OFDeMfgkWHUe2WACJiG6ReSTMoRxcqqgRG4bISlyeoRnT1QeBbZ3EhpExFkAiolvUN8gd3f1dYagzYe1+joQhuhFdZS2+T8kBAPPV9CQGCyAR0S1SKBSYPDAYAPBlUibqjCaxgYgs3PoD2aiqNSLM1wXRoRz9IhILIBHRbXgg3B8ezg7I1VVjy3GOhCG6FqNJwiqOfrEYLIBERLdBY6/Cow0jYVY2XNlIRFfaerwAFy5Vwd3JHqN7c/SLaCyARES36bHoINgpFdifeQlHL+hExyGySJf/QHqkfyA09irBaYgFkIjoNvm4ajCylx8ArgISXc3xXD32ZZRApVRgwoAg0XEILIBERM1iyqD6m9n/dCQXhWXVgtMQWZb4PfV/GA3v4Qt/N0fBaQhgASQiahYRAW7oE+iGWqOEr/ZmiY5DZDFKKmqwMTUXAPA4R79YDBZAIqJm8vjg+lXANfvOw1BnFJyGyDKs2XceNXUm9GqvRZ9Ad9FxqAELIBFRM4nt7gs/rQbF5TX46XCe6DhEwtXUmbA66TwA4PFBIRz9YkFYAImImom9SonHoutPcF/xZwYkSRKciEisn4/morDMAG8XNe7r6Sc6Dv0PFkAiomb0SL9AaOyVOJ6nR3JGieg4RMJIkoTlf9Zf/DExOggOdqwcloT/NoiImpG7swMe6t0eALByd6bYMEQCHTh/CWk5eqjtlHikYVg6WQ4WQCKiZnb5Ssctx/ORXVIpNgyRICsaVv8e6t0ObduoBaeh/5/FF8CSkhKMHz8erq6ucHNzw9SpU1FeXn7d/efMmYMuXbrA0dERgYGBeOqpp6DTNZ7Or1AortjWrl3b0i+HiGSgk48LhnTyhEniKiDJU3ZJJX47lg/gr6vjybJYfAEcP348jh07hq1bt2Lz5s3YtWsXZsyYcc39c3NzkZubi3fffRdpaWmIj49HQkICpk6desW+K1euRF5ennkbPXp0C74SIpKTqQ1veusPZKOsulZwGqLWtTopEyYJGNLJE519XETHoauwEx3gek6cOIGEhATs378fffv2BQB89NFHuO+++/Duu+/C39//iuf06NED33//vfmfO3TogNdffx0TJkxAXV0d7Oz+eslubm7w9fVt+RdCRLJzZ2cvdPRug/TCcqzbn41pQ0JFRyJqFeWGOqxNzgZQP/qFLJNFrwAmJSXBzc3NXP4AICYmBkqlEvv27WvycXQ6HVxdXRuVPwCYNWsWPD090b9/f6xYseKGIxsMBgP0en2jjYjoahQKhfnNL35PJowmjoQhefjuQDbKDHUI9XTGnZ29RMeha7DoApifnw9vb+9Gj9nZ2cHDwwP5+flNOkZxcTFeffXVKz42fuWVV7B+/Xps3boVY8aMwZNPPomPPvrousdavHgxtFqteQsICLi5F0REsvK3Pu3g7mSPC5eqsOVY0/43i8iaGU0SVu7JBABMGRQMpZKDny2VkAK4YMGCq16E8b/byZMnb/v76PV6jBw5Et26dcNLL73U6GsvvvgiBg0ahN69e+OFF17A888/j3feeee6x4uLi4NOpzNv2dnZt52RiGyXxl6F8VH1g6Evz0MjsmWJJwpw/mIltI72GBPZXnQcug4h5wDOnz8fkydPvu4+oaGh8PX1RWFhYaPH6+rqUFJScsNz98rKyjB8+HC4uLhgw4YNsLe3v+7+UVFRePXVV2EwGKBWX/1ydbVafc2vERFdzcToIHy26ywOnL+Ew9mlCA9wEx2JqMV80fCHzqNRgXBysOjLDGRPyL8dLy8veHnd+LyA6OholJaWIiUlBZGRkQCAbdu2wWQyISoq6prP0+v1iI2NhVqtxqZNm6DRaG74vVJTU+Hu7s6CR0TNyttVgwd6+eOHQzlY/mcGPnykt+hIRC3iyIVSJGeUwE6pwKToYNFx6AYs+hzArl27Yvjw4Zg+fTqSk5Oxe/duzJ49G+PGjTNfAZyTk4OwsDAkJycDqC9/w4YNQ0VFBZYvXw69Xo/8/Hzk5+fDaDQCAH766Sd88cUXSEtLQ3p6Oj799FO88cYbmDNnjrDXSkS26/IctF+O5iFPVyU4DVHLuHyawwPh/vDV3njhhcSy+PXZr7/+GrNnz8bQoUOhVCoxZswYfPjhh+av19bW4tSpU6isrJ+2f/DgQfMVwh07dmx0rIyMDAQHB8Pe3h5LlizBM888A0mS0LFjR7z//vuYPn16670wIpKNHu206B/igeSMEsTvyUTciK6iIxE1q9zSKvx8JA/AXzMwybIppBvNPqFr0uv10Gq15jEzRETXsuVYPmZ8mQIXjR2S4oaijdri//4marLFv57AZzvPYUCoB9bOiBYd54b4/m3hHwETEdmKmK4+CPF0Rll1Hdbv5wQBsh0Vhjqs2ZcFAJg2mAPPrQULIBFRK1AqFeZzAVfszkCd0SQ4EVHz+PZANsqq6xDi6Yx7wrxv/ASyCCyARESt5O992psHQ/92rEB0HKLbZjRJWLE7E0D9xU4c/Gw9WACJiFqJo4MKjw2oHwy97I9zN7z9JJGl23q8AFkllXBzsseYPu1Ex6GbwAJIRNSKHosOhoOdEqnZpUg5f0l0HKLbsuyPcwCAR/tz8LO1YQEkImpFXi5qPBRRv1Jy+c2TyBqlnC9ByvlLcFApMXlgsOg4dJNYAImIWtm0IfUXg2w5XoDM4grBaYhuzee76v+Aeah3O3i7cvCztWEBJCJqZZ18XHBXFy9IUv0VwUTW5lxRObYcr7+Q6fIfNGRdWACJiASYPqR+Xtr6A9m4VFEjOA3RzVn+ZwYkCbgnzBudfFxEx6FbwAJIRCTAwA5t0d3fFdW1Jny597zoOERNVlxuwHcpFwAAM+7g4GdrxQJIRCSAQqEwv3nG78lEda1RcCKipvky6TwMdSb0aq9FVIiH6Dh0i1gAiYgEGdnTD+3cHFFSUYNvG1ZUiCxZVY3RvGI9445QKBQc/GytWACJiASxUykxveEE+i/+OAejiYOhybJ9d/ACSipq0N7dEcO7+4qOQ7eBBZCISKCH+wXAzcke5y9W4rdj+aLjEF2T0SRhecPsyqmDQ2CnYoWwZvy3R0QkkJODHSY23B7us51neXs4slgJafnIvFgJraM9Hu4bIDoO3SYWQCIiwSYODIbaTonDF3TYe65EdByiK0iShKU7zwIAJg0MhrOat32zdiyARESCebZR4x992wMAPtt1VnAaoivtTr+Iozk6aOx52zdbwQJIRGQBpg0OhUIB7DhVhJP5etFxiBq5vPo3rl8gPJwdBKeh5sACSERkAYI9nTGiR/1VlZ/tPCc4DdFfjl7Q4c/0YqiUCkwdzNu+2QoWQCIiC/HEnR0AAJsO5yK7pFJwGqJ6l1f/Hgz3R4CHk+A01FxYAImILESv9m4Y3NETRpOEZX9wFZDEyyiuwK9peQCAf97J277ZEhZAIiIL8uRd9auA6/Zno6jMIDgNyd3nu87BJAH3hHkjzNdVdBxqRiyAREQWJLpDW4QHuMFQZ8LK3Rmi45CMFZZV4/uD9bcovHx6AtkOFkAiIguiUCjMq4BfJp2HvrpWcCKSq+V/ZqCmzoTIIHf0C3YXHYeaGQsgEZGFuberDzp5t0GZoQ5f7T0vOg7JUGllDb5Kqv/dm3lnBygUCsGJqLmxABIRWRilUoGZDauAK/7MQHWtUXAikpv4PZmoqDEizNcFQ7t6i45DLYAFkIjIAj0Q7o92bo4oLq/B+gPZouOQjJQb6rBydyYAYNbdHbn6Z6NYAImILJC9Smkeu/HZznOoNZoEJyK5+HrveeiqahHq6Yz7evqJjkMthAWQiMhCPdw3AJ5tHJBTWoUfU3NFxyEZqK41Ytkf9VefP3FXB6iUXP2zVSyAREQWSmOvwrQh9auAn2xPh9EkCU5Etm79gWwUlxvQzs0RD/VuJzoOtSAWQCIiCzZhQBDcnOxxrrgCm49wFZBaTq3RZL4P9T/vDIW9ihXBlln8v92SkhKMHz8erq6ucHNzw9SpU1FeXn7d59x1111QKBSNtieeeKLRPllZWRg5ciScnJzg7e2N5557DnV1dS35UoiIblobtR2mDgoBAHy8LR0mrgJSC9lwKAc5pVXwbKPGw30DRMehFmbxBXD8+PE4duwYtm7dis2bN2PXrl2YMWPGDZ83ffp05OXlmbe3337b/DWj0YiRI0eipqYGe/bswapVqxAfH4+FCxe25EshIrolkwYFw0VjhzOF5Ug4li86Dtkgo0nC0h1nAQDTh4RAY68SnIhamkUXwBMnTiAhIQFffPEFoqKiMHjwYHz00UdYu3YtcnOv/1GIk5MTfH19zZur61/3MNyyZQuOHz+Or776ChERERgxYgReffVVLFmyBDU1NS39soiIboqrxh5TGlYBP9qWDkniKiA1r81HcnGuuAJuTvYYPyBIdBxqBRZdAJOSkuDm5oa+ffuaH4uJiYFSqcS+ffuu+9yvv/4anp6e6NGjB+Li4lBZWdnouD179oSPj4/5sdjYWOj1ehw7duyaxzQYDNDr9Y02IqLW8PigYDg7qHAiT4/fTxSKjkM2xGiS8N/EMwCAaYND0EZtJzgRtQaLLoD5+fnw9m48gdzOzg4eHh7Iz7/2xyCPPvoovvrqK2zfvh1xcXH48ssvMWHChEbH/d/yB8D8z9c77uLFi6HVas1bQADPkSCi1uHm5ICJA4MBAB8mnuEqIDWbzUdyca6oAlpHe0xq+B0j2yekAC5YsOCKizT+/+3kyZO3fPwZM2YgNjYWPXv2xPjx47F69Wps2LABZ8+eva3ccXFx0Ol05i07m9P5iaj1TBscAkd7FY7m6LDjdJHoOGQDjCYJH/7P6p+Lxl5wImotQtZ558+fj8mTJ193n9DQUPj6+qKwsPFHHXV1dSgpKYGvr2+Tv19UVBQAID09HR06dICvry+Sk5Mb7VNQUAAA1z2uWq2GWq1u8vclImpObduoMT4qEF/8mYH//n4Gd3X24m266Lb8fDQPZy+v/g0KFh2HWpGQAujl5QUvL68b7hcdHY3S0lKkpKQgMjISALBt2zaYTCZzqWuK1NRUAICfn5/5uK+//joKCwvNHzFv3boVrq6u6Nat202+GiKi1jPjzlB8te88UrNLseN0Ee7u4n3jJxFdxf+u/k0dHAJXrv7JikWfA9i1a1cMHz4c06dPR3JyMnbv3o3Zs2dj3Lhx8Pf3BwDk5OQgLCzMvKJ39uxZvPrqq0hJSUFmZiY2bdqEiRMn4o477kCvXr0AAMOGDUO3bt3w2GOP4fDhw/jtt9/w73//G7NmzeIKHxFZNG8XDR5ruErzP1tP81xAumW/HM1DemE5XDV2mMzVP9mx6AII1F/NGxYWhqFDh+K+++7D4MGD8fnnn5u/Xltbi1OnTpmv8nVwcMDvv/+OYcOGISwsDPPnz8eYMWPw008/mZ+jUqmwefNmqFQqREdHY8KECZg4cSJeeeWVVn99REQ36593doCjvQpHLuiQyCuC6RaYGq3+hXL1T4YUEv98vGV6vR5arRY6na7RnEEiopb25q8nsXTnWXTzc8XPTw3muYB0UzYfycXsNYfgqrHDHy/cA62jvAog37+tYAWQiIiuNOOOUDg7qHA8T4/fjhWIjkNWxGiS8J+tpwEAjw8OkV35o3osgEREVsjD2cF8d5APfj/NewRTk208lIOzRfV3/Zg6OER0HBKEBZCIyEpNGxICF7UdTuaX4dc03iOYbqymzoQPEutX/564swPn/skYCyARkZVyc3LA44P/WgU0chWQbmD9gWxkl1TBs40aE6N5z185YwEkIrJijw8OgavGDmcKy/HT4VzRcciCVdca8dG2+it/Z9/dAU4OvOevnLEAEhFZMa2jPf55ZwcAwPtbT6OmziQ4EVmqr/aeR4HegHZujngkKlB0HBKMBZCIyMpNGRQMzzZqZJVUYt3+LNFxyAJVGOrw6Y6zAICnhnaE2k4lOBGJxgJIRGTlnBzs8PTQjgCA/yamo7KmTnAisjQrd2fgYkUNgts6YUyf9qLjkAVgASQisgFj+wUi0MMJxeUGrNydKToOWRBdZS0+33UOAPDMvZ1hp+JbP7EAEhHZBAc7Jebd2xkAsHTnWZRW1ghORJZiyY506KvrEObrggd6+YuOQxaCBZCIyEY8GO6PMF8XlFXX4dOdZ0XHIQuQU1qF+D2ZAIAXhodBqeQtA6keCyARkY1QKhV4fngXAED87kzk66oFJyLR3ttyCjV1JgwI9cBdXbxExyELwgJIRGRD7u7ijX7B7jDUmfDfhjs+kDwdz9Vjw6EcAEDciK5QKLj6R39hASQisiEKhQIvDA8DAKzbn40zBWWCE5EobyWchCQBI3v5ITzATXQcsjAsgERENqZvsAdiu/vAJAGLfz0pOg4JsCe9GDtPF8FepcDzsV1ExyELxAJIRGSDXhgeBjulAttOFmJ3erHoONSKTCbJXPzHRwUhqK2z4ERkiVgAiYhsUKhXG0wYEAQAeO3nEzCaJMGJqLVsPpqHozk6tFHbYc49HUXHIQvFAkhEZKOeHtoJrho7nMjT4/uDF0THoVZQXWvEWw2rf/+8IxRt26gFJyJLxQJIRGSj3J0dMOeeTgDqx4HwFnG274s/ziGntAp+Wg2mDQkVHYcsGAsgEZENmzgwCAEejijQG7BsV4boONSCCvTV+GRH/QDwBSPC4OigEpyILBkLIBGRDVPbqcxjYT7bdRaFeg6HtlVvJ5xCZY0RfQLd8GA4b/lG18cCSERk40b29EOfQDdU1hjxVsIp0XGoBRzOLjWf57nwge4c+kw3xAJIRGTjFAoFFj7QHQDw/cELSDlfIjgRNSdJkvDK5uMAgL/1bocIDn2mJmABJCKSgYgANzzctz0AYNGmYxwLY0M2Hc5FyvlLcLRX4fmGj/uJboQFkIhIJp4fHgYXjR3ScvRYtz9bdBxqBlU1f419efKuDvDVagQnImvBAkhEJBOebdR4JqYzAOCd306itLJGcCK6XUu2pyNXV412bo6YfgfHvlDTsQASEcnIY9FB6OzTBpcqa/HeltOi49BtOFtUjs921Y99efH+btDYc+wLNR0LIBGRjNirlHjpwfoLQr7edx7Hc/WCE9GtkCQJC39MQ61Rwt1dvBDb3Ud0JLIyLIBERDIzsIMnRvb0g0kCFm1Kg4kXhFidn47kYXf6RajtlHj5wR4c+0I3jQWQiEiG/jWyKxztVdifeQnfpvCCEGuir67Fqw1jX2bd3RGBbZ0EJyJrxAJIRCRD7dwcMe/e+gtC3vjlJIrLDYITUVP9Z+tpFJUZEOLpjBm88INukcUXwJKSEowfPx6urq5wc3PD1KlTUV5efs39MzMzoVAorrp9++235v2u9vW1a9e2xksiIrIIUwYFo7u/K3RVf60okWU7lqvDqj2ZAICXH+zOCz/olll8ARw/fjyOHTuGrVu3YvPmzdi1axdmzJhxzf0DAgKQl5fXaHv55ZfRpk0bjBgxotG+K1eubLTf6NGjW/jVEBFZDjuVEov/1hNKBfBjai52ni4SHYmuw2iS8H8b0mCSgJG9/HBHZy/RkciKWXQBPHHiBBISEvDFF18gKioKgwcPxkcffYS1a9ciNzf3qs9RqVTw9fVttG3YsAEPP/ww2rRp02hfNze3RvtpNBygSUTy0qu9GyYNDAYA/HvjUVTVGMUGomtauTsDqdmlcFHb4cWR3UTHIStn0QUwKSkJbm5u6Nu3r/mxmJgYKJVK7Nu3r0nHSElJQWpqKqZOnXrF12bNmgVPT0/0798fK1asgCRd/0o4g8EAvV7faCMisnbzh3WBv1aD7JIqfJDI2YCW6PzFCry75RSA+gt4eMcPul0WXQDz8/Ph7e3d6DE7Ozt4eHggPz+/ScdYvnw5unbtioEDBzZ6/JVXXsH69euxdetWjBkzBk8++SQ++uij6x5r8eLF0Gq15i0gIODmXhARkQVqo7bDK6N6AAC++CODswEtjCRJWPD9UVTXmjCwQ1uM68f3Hrp9QgrgggULrnmhxuXt5MmTt/19qqqqsGbNmquu/r344osYNGgQevfujRdeeAHPP/883nnnneseLy4uDjqdzrxlZ3N0AhHZhphuPhjRwxdGk4TnvjuMmjqT6EjU4JvkbCSduwhHexXe/FsvzvyjZmEn4pvOnz8fkydPvu4+oaGh8PX1RWFhYaPH6+rqUFJSAl9f3xt+n++++w6VlZWYOHHiDfeNiorCq6++CoPBALVafdV91Gr1Nb9GRGTtXh7VHUnnLuJYrh4fb083j4khcfJ0VXjjlxMAgGdju3DmHzUbIQXQy8sLXl43vnopOjoapaWlSElJQWRkJABg27ZtMJlMiIqKuuHzly9fjgcffLBJ3ys1NRXu7u4seEQkW94uGrw6qgfmfHMIS7an496uPujZXis6lmxJUv1Vv+WGOvQJdMPkhot1iJqDRZ8D2LVrVwwfPhzTp09HcnIydu/ejdmzZ2PcuHHw9/cHAOTk5CAsLAzJycmNnpueno5du3Zh2rRpVxz3p59+whdffIG0tDSkp6fj008/xRtvvIE5c+a0yusiIrJUD4T7Y2QvPxhNEuatT0V1La8KFuX7gznYdrIQDiol3v57L6iU/OiXmo9FF0AA+PrrrxEWFoahQ4fivvvuw+DBg/H555+bv15bW4tTp06hsrKy0fNWrFiB9u3bY9iwYVcc097eHkuWLEF0dDQiIiLw2Wef4f3338eiRYta/PUQEVm6V0f1gGcbB5wpLMd/fudVwSJkXazEoh/TAABPx3RCR28XwYnI1iikG80+oWvS6/XQarXQ6XRwdXUVHYeIqNlsPV6A6asPQKEAvnsiGpFBHqIjyUad0YSxn+9FyvlL6B/sgW9mDODqXzPj+7cVrAASEVHru7ebD/7Wpx0kCZi//jAqDHWiI8nGpzvOIuX8Jbio7fDew+Esf9QiWACJiOiqFj3QHX5aDTIvVuLFho8jqWWlZpfig8QzAIBXRndHgAev+qWWwQJIRERXpXW0xwdjI6BUAD8czMH3KRdER7JpFYY6zF17CEaThAfC/TE6op3oSGTDWACJiOiaokLb4umh9fMAX/wxDWeLygUnsl2vbj6OzIuV8Ndq8NqoHhz4TC2KBZCIiK5r9j0dER3aFpU1RsxZc4ijYVrADwcvYO3+bCgUwLsPh0PrZC86Etk4FkAiIroulVKBD8ZFwMPZAcfz9FjccGcKah4n8/X414ajAICnh3bCwA6eghORHLAAEhHRDfm4avDew+EAgFVJ55GQlic4kW3QV9di5lcHUV1rwh2dvfDUPZ1ERyKZYAEkIqImubuLN2bcEQqgfjTMmYIywYmsmyRJeP7bI8gorkA7N8f6C2448oVaCQsgERE12XOxXTAg1AMVNUZMX30Auspa0ZGs1hd/ZCDhWD7sVQosGd8HHs4OoiORjLAAEhFRk9mrlFjyaB+0c3NE5sVKPNUwtoRuTtLZi3gz4SQAYOH93RAR4CY2EMkOCyAREd2Utm3U+HxiJDT2Suw8XYS3fzspOpJVOVdUjie+SoHRJGFUhD8mDAgSHYlkiAWQiIhuWnd/Ld75e/1FIZ/tPIcfU3MEJ7IOlypq8Hj8fuiqahER4Ia3xvTivD8SggWQiIhuyQPh/ph5VwcAwPPfHUHK+UuCE1k2Q50R//wyBZkXK9He3RHLJvaFxl4lOhbJFAsgERHdsmeHdcHQMG8Y6kx4PH4/rwy+BkmSsOD7o0jOLIGL2g4rJveDl4tadCySMRZAIiK6ZSqlAh892hsRAW7QVdVi4opk5OmqRMeyOB8mpmPDoRyolAp8MqEPOvu4iI5EMscCSEREt8XJwQ4rJ/dDBy9n5OmqMXF5Mkora0THshir9mTiP7+fBgC8OqoHhnTyEpyIiAWQiIiagbuzA1ZPjYKvqwZnCssxbdUBVNXwnsHr9mdh0aZjAICn7umIR6MCBSciqscCSEREzaKdmyNWPd4frho7HDh/CU98lYLqWvmWwI2HcrDgh/p7/E4fEoJn7u0sOBHRX1gAiYio2XTxdcHyyf3gaK/CztNFsl0J/PVoHuZ/exiSBDw2IAj/uq8rx72QRWEBJCKiZtUv2APxU/rB2UGFP9OLMXllMioMdaJjtZotx/LNd0j5R2R7vPxgd5Y/sjgsgERE1OyiQtti9dQouKjtsC+jBBNXJENfbfv3DV6bnIUnvkpBrVHCA+H+eHNMLyiVLH9keVgAiYioRUQGueOraVFw1dgh5fwlPPbFPlwsN4iO1SIkScJHiWew4IejMEnA2L4B+M/D4VCx/JGFYgEkIqIWEx7ghm9mDIC7kz0OX9Bh9Ce7cdrGhkUbTRIWbTqG97bWj3qZfXdHvDmmJ+xUfIsly8XfTiIialHd/bX49omBCGrrhOySKvztkz3YfrJQdKxmUVlThznfHMTqpPNQKICXH+yOZ2O78Jw/sngsgERE1OI6erfBxicHISrEA+WGOkxdtR9f/HEOkiSJjnbL0gvLMerj3fjlaD4cVEp89EhvTBoYLDoWUZOwABIRUatwd3bAl1OjMK5fAEwS8NrPJzD/28Mot8IrhDcdzsWDH/+JM4Xl8HZR4+vpUbi/l7/oWERNxgJIREStxsFOicV/64kX7+8GpQL44WAORvx3F/ZnloiO1iSGOiMW/ZiGp745hMoaI6JD2+Lnp4agX7CH6GhEN0UhWfP6u2B6vR5arRY6nQ6urq6i4xARWZXkjBLMW5+KC5eqoFAAT9zZAc/EdIaDnWWuTRzILMG/NhzF6YJyAPUXezxzb2de6WuF+P7NAnhb+AtERHR7yqpr8cpPx/FtygUAQFc/V7w2ujsigyxnRa20sgZvJZzEN8nZAIC2zg545x+9cE+Yj+BkdKv4/s0CeFv4C0RE1DwS0vLxrw1HUVJRAwAY0cMXLwwPQ7Cns7BMJpOEHw/n4PWfT6C4vD7XuH4BWDAiDG5ODsJy0e3j+zcL4G3hLxARUfMpKjPgvS2nsP5ANkwSYKdUYMKAIDw1tBM8nFuvcNUaTdiUmotPdqTjbFEFgPqrmN94qCf6h1jOyiTdOr5/W8FFIK+//joGDhwIJycnuLm5Nek5kiRh4cKF8PPzg6OjI2JiYnDmzJlG+5SUlGD8+PFwdXWFm5sbpk6divLy8hZ4BURE1BReLmq8OaYXfn36DtzVxQt1JgnxezIRvTgR89cfxsGsSy06Nqa61ogv957H3e/uwPxvD+NsUQVcNXZ4LrYLfnlqCMsf2RSLXwFctGgR3NzccOHCBSxfvhylpaU3fM5bb72FxYsXY9WqVQgJCcGLL76Io0eP4vjx49BoNACAESNGIC8vD5999hlqa2sxZcoU9OvXD2vWrGlyNv4FQUTUcv48U4y3Ek7iaI7O/FhXP1c8GhWIe7v6wFerue3vUV1rxI5TRfjlaB4STxSgosYIAPBs44Cpg0MxYUAgXDT2t/19yLLw/dsKCuBl8fHxmDt37g0LoCRJ8Pf3x/z58/Hss88CAHQ6HXx8fBAfH49x48bhxIkT6NatG/bv34++ffsCABISEnDffffhwoUL8Pdv2iwn/gIREbUsSZJwKLsUX+/NwuYjuTDUmcxfC/VyxqAOnhjYoS0iAt3g7aK54RW5xeUGnMjT40SeHoezddhxqtBc+gCgvbsjpg0Owdh+gXB0ULXY6yKx+P4N2IkO0NwyMjKQn5+PmJgY82NarRZRUVFISkrCuHHjkJSUBDc3N3P5A4CYmBgolUrs27cPDz300FWPbTAYYDD8dSNzvV7fci+EiIigUCjQJ9AdfQLd8eL9XfH9wRxsSs3B0RwdzhVV4FxRBb7cex4AoFIq4OOihq9WAx9XDUyShKpaE6prjaiuNSJfV43CMsMV36OdmyNG9PDFyF5+iAhw423cSBZsrgDm5+cDAHx8Gl+e7+PjY/5afn4+vL29G33dzs4OHh4e5n2uZvHixXj55ZebOTERETWFm5MDpg4OwdTBIdBV1mJvxkUknb2I3enFOFdcAaNJQq6uGrm66mseQ6EAgts6o6ufC7r6umJQJ0/0ZukjGRJSABcsWIC33nrruvucOHECYWFhrZSoaeLi4jBv3jzzP+v1egQEBAhMREQkT1one8R290Vsd18AgNEkoajMgDxdlXmlT6lUwNFeBY29Eo72Krg7O6CLjwuc1Ta39kF004T8VzB//nxMnjz5uvuEhobe0rF9fev/x6CgoAB+fn7mxwsKChAREWHep7CwsNHz6urqUFJSYn7+1ajVaqjV6lvKRURELUelVMBXq2mWC0OI5EBIAfTy8oKXl1eLHDskJAS+vr5ITEw0Fz69Xo99+/Zh5syZAIDo6GiUlpYiJSUFkZGRAIBt27bBZDIhKiqqRXIRERERWQqLnwOYlZWF1NRUZGVlwWg0IjU1FampqY1m9oWFhWHDhg0A6k8Ynjt3Ll577TVs2rQJR48excSJE+Hv74/Ro0cDALp27Yrhw4dj+vTpSE5Oxu7duzF79myMGzeuyVcAExEREVkriz8RYuHChVi1apX5n3v37g0A2L59O+666y4AwKlTp6DT/TUn6vnnn0dFRQVmzJiB0tJSDB48GAkJCeYZgADw9ddfY/bs2Rg6dCiUSiXGjBmDDz/8sHVeFBEREZFAVjMH0BJxjhAREZH14fu3FXwETERERETNiwWQiIiISGZYAImIiIhkhgWQiIiISGZYAImIiIhkhgWQiIiISGZYAImIiIhkhgWQiIiISGZYAImIiIhkxuJvBWfJLt9ERa/XC05CRERETXX5fVvON0NjAbwNZWVlAICAgADBSYiIiOhmlZWVQavVio4hBO8FfBtMJhNyc3Ph4uIChULRrMfW6/UICAhAdna2bO9TeC382Vwffz7Xx5/P9fHnc2382VyfNf18JElCWVkZ/P39oVTK82w4rgDeBqVSifbt27fo93B1dbX4/5BE4c/m+vjzuT7+fK6PP59r48/m+qzl5yPXlb/L5Fl7iYiIiGSMBZCIiIhIZlgALZRarcaiRYugVqtFR7E4/NlcH38+18efz/Xx53Nt/NlcH38+1oUXgRARERHJDFcAiYiIiGSGBZCIiIhIZlgAiYiIiGSGBZCIiIhIZlgALdCSJUsQHBwMjUaDqKgoJCcni45kEXbt2oUHHngA/v7+UCgU2Lhxo+hIFmXx4sXo168fXFxc4O3tjdGjR+PUqVOiY1mMTz/9FL169TIPqY2Ojsavv/4qOpZFevPNN6FQKDB37lzRUSzCSy+9BIVC0WgLCwsTHcui5OTkYMKECWjbti0cHR3Rs2dPHDhwQHQsug4WQAuzbt06zJs3D4sWLcLBgwcRHh6O2NhYFBYWio4mXEVFBcLDw7FkyRLRUSzSzp07MWvWLOzduxdbt25FbW0thg0bhoqKCtHRLEL79u3x5ptvIiUlBQcOHMA999yDUaNG4dixY6KjWZT9+/fjs88+Q69evURHsSjdu3dHXl6eefvzzz9FR7IYly5dwqBBg2Bvb49ff/0Vx48fx3vvvQd3d3fR0eg6OAbGwkRFRaFfv374+OOPAdTfbzggIABz5szBggULBKezHAqFAhs2bMDo0aNFR7FYRUVF8Pb2xs6dO3HHHXeIjmORPDw88M4772Dq1Kmio1iE8vJy9OnTB5988glee+01RERE4IMPPhAdS7iXXnoJGzduRGpqqugoFmnBggXYvXs3/vjjD9FR6CZwBdCC1NTUICUlBTExMebHlEolYmJikJSUJDAZWSOdTgegvuRQY0ajEWvXrkVFRQWio6NFx7EYs2bNwsiRIxv9bxDVO3PmDPz9/REaGorx48cjKytLdCSLsWnTJvTt2xf/+Mc/4O3tjd69e2PZsmWiY9ENsABakOLiYhiNRvj4+DR63MfHB/n5+YJSkTUymUyYO3cuBg0ahB49eoiOYzGOHj2KNm3aQK1W44knnsCGDRvQrVs30bEswtq1a3Hw4EEsXrxYdBSLExUVhfj4eCQkJODTTz9FRkYGhgwZgrKyMtHRLMK5c+fw6aefolOnTvjtt98wc+ZMPPXUU1i1apXoaHQddqIDEFHzmzVrFtLS0nie0v+nS5cuSE1NhU6nw3fffYdJkyZh586dsi+B2dnZePrpp7F161ZoNBrRcSzOiBEjzP9/r169EBUVhaCgIKxfv56nD6D+D86+ffvijTfeAAD07t0baWlpWLp0KSZNmiQ4HV0LVwAtiKenJ1QqFQoKCho9XlBQAF9fX0GpyNrMnj0bmzdvxvbt29G+fXvRcSyKg4MDOnbsiMjISCxevBjh4eH473//KzqWcCkpKSgsLESfPn1gZ2cHOzs77Ny5Ex9++CHs7OxgNBpFR7Qobm5u6Ny5M9LT00VHsQh+fn5X/BHVtWtXfkxu4VgALYiDgwMiIyORmJhofsxkMiExMZHnKdENSZKE2bNnY8OGDdi2bRtCQkJER7J4JpMJBoNBdAzhhg4diqNHjyI1NdW89e3bF+PHj0dqaipUKpXoiBalvLwcZ8+ehZ+fn+goFmHQoEFXjJw6ffo0goKCBCWipuBHwBZm3rx5mDRpEvr27Yv+/fvjgw8+QEVFBaZMmSI6mnDl5eWN/uLOyMhAamoqPDw8EBgYKDCZZZg1axbWrFmDH3/8ES4uLubzRrVaLRwdHQWnEy8uLg4jRoxAYGAgysrKsGbNGuzYsQO//fab6GjCubi4XHGuqLOzM9q2bctzSAE8++yzeOCBBxAUFITc3FwsWrQIKpUKjzzyiOhoFuGZZ57BwIED8cYbb+Dhhx9GcnIyPv/8c3z++eeio9H1SGRxPvroIykwMFBycHCQ+vfvL+3du1d0JIuwfft2CcAV26RJk0RHswhX+9kAkFauXCk6mkV4/PHHpaCgIMnBwUHy8vKShg4dKm3ZskV0LIt15513Sk8//bToGBZh7Nixkp+fn+Tg4CC1a9dOGjt2rJSeni46lkX56aefpB49ekhqtVoKCwuTPv/8c9GR6AY4B5CIiIhIZngOIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHMsAASERERyQwLIBEREZHM/D8SyZeRmXkCFwAAAABJRU5ErkJggg=="
/>
</marimo-cell-output>
<marimo-ui-element
object-id="58cc6f6f-65df-4f76-9e09-d5923a3238f7"
random-id="58cc6f6f-65df-4f76-9e09-d5923a3238f7"
><marimo-code-editor
data-initial-value='"&#92;n# Also run expensive outputs without performing them in the browser&#92;nimport matplotlib.pyplot as plt&#92;nimport numpy as np&#92;nx = np.linspace(0, 2*np.pi, 100)&#92;ny = np.sin(x)&#92;nplt.plot(x, y)&#92;nplt.gca()&#92;n"'
data-label="null"
data-language='"python"'
data-placeholder='""'
data-disabled="true"
></marimo-code-editor
></marimo-ui-element>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="PKri" data-reactive="true">
<marimo-cell-output>
<span class="markdown"
><span class="paragraph">Slider value: 0</span></span
>
</marimo-cell-output>
<marimo-cell-code hidden
>%0Amo.md(f%22Slider%20value%3A%20%7Bslider.value%7D%22)%0A</marimo-cell-code
>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="Xref" data-reactive="true">
<marimo-cell-output>
<span class="markdown"
><h1 id="hello-markdown">Hello, Markdown!</h1>
<span class="paragraph"
>Use marimo's "<code>md</code>" function to embed rich text into
your marimo apps. This function compiles Markdown into HTML that
marimo can display.</span
>
<span class="paragraph"
>For example, here's the code that rendered the above title and
paragraph:</span
>
<div class="codehilite">
<pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span>
<span class="w"> </span><span class="sd">&#39;&#39;&#39;</span>
<span class="sd"> # Hello, Markdown!</span>
<span class="sd"> Use marimo&#39;s &quot;`md`&quot; function to embed rich text into your marimo</span>
<span class="sd"> apps. This function compiles your Markdown into HTML that marimo</span>
<span class="sd"> can display.</span>
<span class="sd"> &#39;&#39;&#39;</span>
<span class="p">)</span>
</code></pre>
</div></span
>
</marimo-cell-output>
<marimo-cell-code hidden
>%0Amo.md(%0A%20%20%20%20%22%22%22%0A%20%20%20%20%23%20Hello%2C%20Markdown!%0A%0A%20%20%20%20Use%20marimo's%20%22%60md%60%22%20function%20to%20embed%20rich%20text%20into%20your%20marimo%0A%20%20%20%20apps.%20This%20function%20compiles%20Markdown%20into%20HTML%20that%20marimo%0A%20%20%20%20can%20display.%0A%0A%20%20%20%20For%20example%2C%20here's%20the%20code%20that%20rendered%20the%20above%20title%20and%0A%20%20%20%20paragraph%3A%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20%20%20%20%20%23%20Hello%2C%20Markdown!%0A%0A%20%20%20%20%20%20%20%20Use%20marimo's%20%22%60md%60%22%20function%20to%20embed%20rich%20text%20into%20your%20marimo%0A%20%20%20%20%20%20%20%20apps.%20This%20function%20compiles%20your%20Markdown%20into%20HTML%20that%20marimo%0A%20%20%20%20%20%20%20%20can%20display.%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20)%0A%20%20%20%20%60%60%60%0A%20%20%20%20%22%22%22%0A)%0A</marimo-cell-code
>
</marimo-island>
<marimo-island data-app-id="main" data-cell-id="SFPL" data-reactive="true">
<marimo-cell-output>
<span class="markdown"
><h2 id="latex">LaTeX</h2>
<span class="paragraph">You can embed LaTeX in Markdown.</span>
<span class="paragraph">For example,</span>
<div class="codehilite">
<pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span><span class="sa">r</span><span class="s1">&#39;$f : \mathbf</span><span class="si">{R}</span><span class="s1"> o \mathbf</span><span class="si">{R}</span><span class="s1">$&#39;</span><span class="p">)</span>
</code></pre>
</div>
<span class="paragraph"
>renders
<marimo-tex class="arithmatex"
>||(f : \mathbf{R} o \mathbf{R}||)</marimo-tex
>, while</span
>
<div class="codehilite">
<pre><span></span><code><span class="n">mo</span><span class="o">.</span><span class="n">md</span><span class="p">(</span>
<span class="w"> </span><span class="sa">r</span><span class="sd">&#39;&#39;&#39;</span>
<span class="sd"> \[</span>
<span class="sd"> f: \mathbf{R} o \mathbf{R}</span>
<span class="sd"> \]</span>
<span class="sd"> &#39;&#39;&#39;</span>
<span class="p">)</span>
</code></pre>
</div>
<span class="paragraph">renders the display math</span>
<marimo-tex class="arithmatex"
>||[ f: \mathbf{R} o \mathbf{R}. ||]</marimo-tex
></span
>
</marimo-cell-output>
<marimo-cell-code hidden
>%0Amo.md(%0A%20%20%20%20r%22%22%22%0A%20%20%20%20%23%23%20LaTeX%0A%20%20%20%20You%20can%20embed%20LaTeX%20in%20Markdown.%0A%0A%20%20%20%20For%20example%2C%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(r'%24f%20%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%24')%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20renders%20%24f%20%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%24%2C%20while%0A%0A%20%20%20%20%60%60%60python3%0A%20%20%20%20mo.md(%0A%20%20%20%20%20%20%20%20r'''%0A%20%20%20%20%20%20%20%20%5C%5B%0A%20%20%20%20%20%20%20%20f%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D%0A%20%20%20%20%20%20%20%20%5C%5D%0A%20%20%20%20%20%20%20%20'''%0A%20%20%20%20)%0A%20%20%20%20%60%60%60%0A%0A%20%20%20%20renders%20the%20display%20math%0A%0A%20%20%20%20%5C%5B%0A%20%20%20%20f%3A%20%5Cmathbf%7BR%7D%20%09o%20%5Cmathbf%7BR%7D.%0A%20%20%20%20%5C%5D%0A%20%20%20%20%22%22%22%0A)%0A</marimo-cell-code
>
</marimo-island>
<br />
<br />
<br />
<br />
<hr />
<div class="bg-blue-500 p-4 border-2 border-red-500 bg-background">
this should not be affected by global tailwind styles
</div>
<div class="marimo">
<div class="bg-background p-4 border-2 border-red-500 text-foreground">
this should be affected by global tailwind styles
</div>
</div>
<div class="marimo">
<div class="dark">
<div class="bg-background p-4 border-2 border-red-500 text-foreground">
this should be affected by global tailwind styles (dark)
</div>
</div>
</div>
</body>
</html>
```
File: /Users/morganmcguire/ML/marimo/frontend/e2e-tests/tsconfig.json
```json
{
"compilerOptions": {
"lib": ["ES2022", "DOM"],
"noEmit": true,
"module": "ESNext",
"moduleResolution": "node",
"types": ["@types/node"],
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"useUnknownInCatchVariables": true,
"skipLibCheck": true
},
"include": ["*"]
}
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/development.md
```md
# Marimo Islands
marimo islands are a way to render HTML "islands" each with static outputs and code. This differs from the normal "app mode" in the sense that there is no top-level app. In "island mode", we do not have access to the HTML of the parent page.
## Development
**Islands demo page**
```bash
pnpm dev:islands
```
**Generate an HTML page with islands**
```bash
# Generate
uv run ./islands/generate.py > islands/__demo__/index.html
# Run the Vite server
pnpm dev:islands
```
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/generate.py
```py
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "marimo",
# ]
# [tool.uv.sources]
# marimo = { path = "../../", editable = true }
# ///
import asyncio
from textwrap import dedent
import marimo
generator = marimo.MarimoIslandGenerator()
def run():
stubs = [
# Basic
generator.add_code("import marimo as mo"),
generator.add_code("mo.md('Hello, islands!')"),
# Slider
generator.add_code(
"""
slider = mo.ui.slider(0, 100, 2)
slider
"""
),
generator.add_code(
"""
mo.md(f"Slider value: {slider.value}")
"""
),
# display_code=True
generator.add_code("""
mo.md("We can also show the island code!")
""", display_code=True),
# is_reactive=False
generator.add_code("""
# Also run expensive outputs without performing them in the browser
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 2*np.pi, 100)
y = np.sin(x)
plt.plot(x, y)
plt.gca()
""", display_code=True, is_reactive=False),
# Error
generator.add_code(
"""
import idk_package
"Should raise an error"
"""
),
# Markdown
generator.add_code(
"""
mo.md(
\"\"\"
# Hello, Markdown!
Use marimo's "`md`" function to embed rich text into your marimo
apps. This function compiles Markdown into HTML that marimo
can display.
For example, here's the code that rendered the above title and
paragraph:
```python3
mo.md(
'''
# Hello, Markdown!
Use marimo's "`md`" function to embed rich text into your marimo
apps. This function compiles your Markdown into HTML that marimo
can display.
'''
)
```
\"\"\"
)
"""
),
# LaTeX
generator.add_code(
"""
mo.md(
r\"\"\"
## LaTeX
You can embed LaTeX in Markdown.
For example,
```python3
mo.md(r'$f : \mathbf{R} \to \mathbf{R}$')
```
renders $f : \mathbf{R} \to \mathbf{R}$, while
```python3
mo.md(
r'''
\[
f: \mathbf{R} \to \mathbf{R}
\]
'''
)
```
renders the display math
\[
f: \mathbf{R} \to \mathbf{R}.
\]
\"\"\"
)
"""
),
]
app = asyncio.run(generator.build())
NEW_LINE = "\n"
output = f"""
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="a marimo app" />
<title>🏝️</title>
{generator.render_head()}
<!-- If running a local server of the production build -->
<!-- <script type="module" src="http://127.0.0.1:8001/main.js"></script>
<link
href="http://127.0.0.1:8001/style.css"
rel="stylesheet"
crossorigin="anonymous"
/> -->
<!-- If running from Vite -->
<!-- <script type="module" src="/src/core/islands/main.ts"></script> -->
</head>
<body>
{dedent(NEW_LINE.join([stub.render() for stub in stubs]))}
<br />
<br />
<br />
<br />
<hr />
<div class="bg-background p-4 border-2 text-primary font-bold bg-background">
this should not be affected by global tailwind styles
</div>
<div class="marimo">
<div class="bg-background p-4 border-2 text-primary font-bold text-foreground">
this should be affected by global tailwind styles
</div>
</div>
<div class="marimo">
<div class="dark">
<div class="bg-background p-4 border-2 text-primary font-bold text-foreground">
this should be affected by global tailwind styles (dark)
</div>
</div>
</div>
</div>
</body>
</html>
"""
print(output)
if __name__ == "__main__":
run()
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/vite.config.mts
```mts
/* Copyright 2024 Marimo. All rights reserved. */
import { type Plugin, defineConfig } from "vite";
import fs from "node:fs";
import path from "node:path";
import react from "@vitejs/plugin-react";
import tsconfigPaths from "vite-tsconfig-paths";
import packageJson from "../package.json";
const htmlDevPlugin = (): Plugin => {
return {
apply: "serve",
name: "html-transform",
transformIndexHtml: async () => {
const indexHtml = await fs.promises.readFile(
path.resolve(__dirname, "__demo__", "index.html"),
"utf-8",
);
return `<!DOCTYPE html>\n${indexHtml}`;
},
};
};
const ReactCompilerConfig = {
target: "18",
};
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
dedupe: ["react", "react-dom", "@emotion/react", "@emotion/cache"],
},
worker: {
format: "es",
plugins: () => [tsconfigPaths()],
},
define: {
"process.env": {
NODE_ENV: JSON.stringify(process.env.NODE_ENV),
},
"import.meta.env.VITE_MARIMO_ISLANDS": JSON.stringify(true),
// Precedence: VITE_MARIMO_VERSION > package.json version > "latest"
"import.meta.env.VITE_MARIMO_VERSION": process.env.VITE_MARIMO_VERSION
? JSON.stringify(process.env.VITE_MARIMO_VERSION)
: process.env.NODE_ENV === "production"
? JSON.stringify(packageJson.version)
: JSON.stringify("latest"),
},
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
plugins: [
htmlDevPlugin(),
react({
babel: {
presets: ["@babel/preset-typescript"],
plugins: [
["@babel/plugin-proposal-decorators", { legacy: true }],
["@babel/plugin-proposal-class-properties", { loose: true }],
["babel-plugin-react-compiler", ReactCompilerConfig],
],
},
}),
tsconfigPaths(),
],
build: {
emptyOutDir: true,
lib: {
entry: path.resolve(__dirname, "../src/core/islands/main.ts"),
formats: ["es"],
},
rollupOptions: {
output: {
// Remove hash from entry file name, so it's easier to import
entryFileNames: "[name].js",
},
},
},
});
```
File: /Users/morganmcguire/ML/marimo/frontend/islands/validate.sh
```sh
#!/bin/sh
# Validate
# - no process.env variables in any of the js files
# - version does not equal 0.0.0-placeholder
# - typescript data uris are not converted to data:video/mp2t
# - files dist/main.js and dist/style.css exist
OUT_DIR=$(pwd)/dist
echo "validating $OUT_DIR"
echo "[validate: no process.env variables in any of the js files]"
grep -R "process.env" $(pwd)/dist
if [ $? -eq 0 ]; then
echo "process.env variables found in js files"
exit 1
fi
echo "[validate: version does not equal 0.0.0-placeholder]"
grep -R "0.0.0-placeholder" $(pwd)/dist
if [ $? -eq 0 ]; then
echo "version is 0.0.0-placeholder"
exit 1
fi
echo "[validate: data uri does not contain data:video/mp2t]"
grep -R "data:video/mp2t" $(pwd)/dist
if [ $? -eq 0 ]; then
echo "mininification misencoded typescript data uri."
echo "Try naming the file with a .tsx extension."
exit 1
fi
echo "[validate: files dist/main.js and dist/style.css exist]"
if [ ! -f "$OUT_DIR/main.js" ] || [ ! -f "$OUT_DIR/style.css" ]; then
echo "dist/main.js or dist/style.css does not exist"
exit 1
fi
echo "validation passed"
```
File: /Users/morganmcguire/ML/marimo/frontend/public/files/wasm-intro.py
```py
# Copyright 2024 Marimo. All rights reserved.
import marimo
__generated_with = "0.2.6"
app = marimo.App()
@app.cell
def __():
import marimo as mo
mo.md("# Welcome to [marimo](https://github.com/marimo-team/marimo)! 🌊🍃")
return mo,
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
**This marimo notebook is powered by [WASM](https://webassembly.org/)**:
it's running entirely in your browser!
"""
).callout()
return
@app.cell(hide_code=True)
def __(mo):
mo.accordion(
{
"When should I use WASM notebooks?": """
WASM notebooks are excellent for sharing:
because they run entirely in the browser, they are extremely
easy to share. Just open the notebook action menu at the top-right and
click the "Share WASM notebook" button to get a shareable URL. You can
also embed your notebook in other webpages via an `<iframe>`.
WASM notebooks are well-suited for quickly experimenting with code
and models, doing lightweight data analysis, authoring blog
posts, tutorials, and educational articles, and even building internal
tools. They are not well-suited for notebooks that do heavy
computation, and they don't support multi-threading nor
multiprocessing; for these cases, use a regular marimo notebook.
"""
}
)
return
@app.cell(hide_code=True)
def __(mo):
mo.accordion(
{
"Installing packages in WASM notebooks": mo.md(
"""
Many packages will be automatically installed,
These can be found at [packages-in-pyodide](https://pyodide.org/en/stable/usage/packages-in-pyodide.html).
For other packages, use micropip:
```python
import micropip
await micropip.install("plotly")
import plotly
```
"""
)
}
)
return
@app.cell
def __(mo):
slider = mo.ui.slider(1, 22)
return slider,
@app.cell
def __(mo, slider):
mo.md(
f"""
marimo is a **reactive** Python notebook.
This means that unlike traditional notebooks, marimo notebooks **run
automatically** when you modify them or
interact with UI elements, like this slider: {slider}.
{"##" + "🍃" * slider.value}
"""
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
## 1. Reactive execution
A marimo notebook is made up of small blocks of Python code called
cells.
marimo reads your cells and models the dependencies among them: whenever
a cell that defines a global variable is run, marimo
**automatically runs** all cells that reference that variable.
Reactivity keeps your program state and outputs in sync with your code,
making for a dynamic programming environment that prevents bugs before they
happen.
"""
)
return
@app.cell(hide_code=True)
def __(changed, mo):
(
mo.md(
f"""
**✨ Nice!** The value of `changed` is now {changed}.
When you updated the value of the variable `changed`, marimo
**reacted** by running this cell automatically, because this cell
references the global variable `changed`.
Reactivity ensures that your notebook state is always
consistent, which is crucial for doing good science; it's also what
enables marimo notebooks to double as tools and apps.
"""
)
if changed
else mo.md(
"""
**🌊 See it in action.** In the next cell, change the value of the
variable `changed` to `True`, then click the run button.
"""
)
)
return
@app.cell
def __():
changed = False
return changed,
@app.cell(hide_code=True)
def __(mo):
mo.accordion(
{
"Tip: execution order": (
"""
The order of cells on the page has no bearing on
the order in which cells are executed: marimo knows that a cell
reading a variable must run after the cell that defines it. This
frees you to organize your code in the way that makes the most
sense for you.
"""
)
}
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
**Global names must be unique.** To enable reactivity, marimo imposes a
constraint on how names appear in cells: no two cells may define the same
variable.
"""
)
return
@app.cell(hide_code=True)
def __(mo):
mo.accordion(
{
"Tip: encapsulation": (
"""
By encapsulating logic in functions, classes, or Python modules,
you can minimize the number of global variables in your notebook.
"""
)
}
)
return
@app.cell(hide_code=True)
def __(mo):
mo.accordion(
{
"Tip: private variables": (
"""
Variables prefixed with an underscore are "private" to a cell, so
they can be defined by multiple cells.
"""
)
}
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""## 2. UI elements
Cells can output interactive UI elements. Interacting with a UI
element **automatically triggers notebook execution**: when
you interact with a UI element, its value is sent back to Python, and
every cell that references that element is re-run.
marimo provides a library of UI elements to choose from under
`marimo.ui`.
"""
)
return
@app.cell
def __(mo):
mo.md("**🌊 Some UI elements.** Try interacting with the below elements.")
return
@app.cell
def __(mo):
icon = mo.ui.dropdown(["🍃", "🌊", "✨"], value="🍃")
return icon,
@app.cell
def __(icon, mo):
repetitions = mo.ui.slider(1, 16, label=f"number of {icon.value}: ")
return repetitions,
@app.cell
def __(icon, repetitions):
icon, repetitions
return
@app.cell
def __(icon, mo, repetitions):
mo.md("# " + icon.value * repetitions.value)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
## 3. marimo is just Python
marimo cells parse Python (and only Python), and marimo notebooks are
stored as pure Python files — outputs are _not_ included. There's no
magical syntax.
The Python files generated by marimo are:
- easily versioned with git, yielding minimal diffs
- legible for both humans and machines
- formattable using your tool of choice,
- usable as Python scripts, with UI elements taking their default
values, and
- importable by other modules (more on that in the future).
"""
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
## 4. Running notebooks as apps
marimo notebooks can double as apps. Click the app window icon in the
bottom-right to see this notebook in "app view."
Serve a notebook as an app with `marimo run` at the command-line.
Of course, you can use marimo just to level-up your
notebooking, without ever making apps.
"""
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""## 5. The `marimo` command-line tool
**Creating and editing notebooks.** Use
```
marimo edit
```
in a terminal to create a new marimo notebook, or
```
marimo edit notebook.py
```
to create/edit a notebook called `notebook.py`.
**Running as apps.** Use
```
marimo run notebook.py
```
to start a webserver that serves your notebook as an app in read-only mode,
with code cells hidden.
**Convert a Jupyter notebook.** Convert a Jupyter notebook to a marimo
notebook using `marimo convert`:
```
marimo convert your_notebook.ipynb > your_app.py
```
**Tutorials.** marimo comes packaged with tutorials:
- `dataflow`: more on marimo's automatic execution
- `ui`: how to use UI elements
- `markdown`: how to write markdown, with interpolated values and
LaTeX
- `plots`: how plotting works in marimo
- `sql`: how to use SQL
- `layout`: layout elements in marimo
- `fileformat`: how marimo's file format works
- `markdown-format`: for using `.md` files in marimo
- `for-jupyter-users`: if you are coming from Jupyter
Start a tutorial with `marimo tutorial`; for example,
```
marimo tutorial dataflow
```
In addition to tutorials, we have examples in our
[our GitHub repo](https://www.github.com/marimo-team/marimo/tree/main/examples).
"""
)
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
## 6. The marimo editor
Here are some tips to help you get started with the marimo editor.
"""
)
return
@app.cell
def __(mo, tips):
mo.accordion(tips)
return
@app.cell
def __(mo):
mo.md("## Finally, a fun fact")
return
@app.cell(hide_code=True)
def __(mo):
mo.md(
"""
The name "marimo" is a reference to a type of algae that, under
the right conditions, clumps together to form a small sphere
called a "marimo moss ball". Made of just strands of algae, these
beloved assemblages are greater than the sum of their parts.
"""
)
return
@app.cell(hide_code=True)
def __():
tips = {
"Saving": (
"""
**Saving**
- _Name_ your app using the box at the top of the screen, or
with `Ctrl/Cmd+s`. You can also create a named app at the
command line, e.g., `marimo edit app_name.py`.
- _Save_ by clicking the save icon on the bottom left, or by
inputting `Ctrl/Cmd+s`. By default marimo is configured
to autosave.
"""
),
"Running": (
"""
1. _Run a cell_ by clicking the play ( ▷ ) button on the bottom
right of a cell, or by inputting `Ctrl/Cmd+Enter`.
2. _Run a stale cell_ by clicking the yellow run button to the
right of the cell, or by inputting `Ctrl/Cmd+Enter`. A cell is
stale when its code has been modified but not run.
3. _Run all stale cells_ by clicking the play ( ▷ ) button on
the bottom right of the screen, or input `Ctrl/Cmd+Shift+r`.
"""
),
"Console Output": (
"""
Console output (e.g., `print()` statements) is shown below a
cell.
"""
),
"Creating, Moving, and Deleting Cells": (
"""
1. _Create_ a new cell above or below a given one by clicking
the plus button to the left of the cell, which appears on
mouse hover.
2. _Move_ a cell up or down by dragging on the handle to the
right of the cell, which appears on mouse hover.
3. _Delete_ a cell by clicking the trash bin icon. Bring it
back by clicking the undo button on the bottom right of the
screen, or with `Ctrl/Cmd+Shift+z`.
"""
),
"Disabling Cells": (
"""
You can disable a cell via the cell context menu (open it
by clicking the icon to the right of a cell). marimo will
never run a disabled cell or any cells that depend on it. This
can help prevent accidental execution of expensive computations
when editing a notebook.
"""
),
"Code Folding": (
"""
You can collapse or fold the code in a cell by clicking the arrow
icons in the line number column to the left, or by using keyboard
shortcuts.
Use the command palette (`Ctrl/Cmd+k`) or a keyboard shortcut to
quickly fold or unfold all cells.
"""
),
"Code Formatting": (
"""
If you have [ruff](https://github.com/astral-sh/ruff) installed,
you can format a cell with the keyboard shortcut `Ctrl/Cmd+b`.
"""
),
"Command Palette": (
"""
Use `Ctrl/Cmd+k` to open the command palette.
"""
),
"Keyboard Shortcuts": (
"""
Click the keyboard button on the bottom left of the screen (or
input `Ctrl/Cmd+Shift+h`) to view a list of all keyboard
shortcuts.
"""
),
"Configuration": (
"""
Configure the editor by clicking the gears icon near the top-right
of the screen.
"""
),
}
return tips,
if __name__ == "__main__":
app.run()
```
File: /Users/morganmcguire/ML/marimo/frontend/patches/react-plotly.js.patch
```patch
diff --git a/factory.js b/factory.js
index 40f3edccbebc45b491dd3488ed619ec9208be8d9..f88f451588d656805d1ae76ce4184a486a179eb1 100644
--- a/factory.js
+++ b/factory.js
@@ -40,8 +40,8 @@ function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.g
// The naming convention is:
// - events are attached as `'plotly_' + eventName.toLowerCase()`
// - react props are `'on' + eventName`
-var eventNames = ['AfterExport', 'AfterPlot', 'Animated', 'AnimatingFrame', 'AnimationInterrupted', 'AutoSize', 'BeforeExport', 'BeforeHover', 'ButtonClicked', 'Click', 'ClickAnnotation', 'Deselect', 'DoubleClick', 'Framework', 'Hover', 'LegendClick', 'LegendDoubleClick', 'Relayout', 'Relayouting', 'Restyle', 'Redraw', 'Selected', 'Selecting', 'SliderChange', 'SliderEnd', 'SliderStart', 'SunburstClick', 'Transitioning', 'TransitionInterrupted', 'Unhover', 'WebGlContextLost'];
-var updateEvents = ['plotly_restyle', 'plotly_redraw', 'plotly_relayout', 'plotly_relayouting', 'plotly_doubleclick', 'plotly_animated', 'plotly_sunburstclick']; // Check if a window is available since SSR (server-side rendering)
+var eventNames = ['AfterExport', 'AfterPlot', 'Animated', 'AnimatingFrame', 'AnimationInterrupted', 'AutoSize', 'BeforeExport', 'BeforeHover', 'ButtonClicked', 'Click', 'ClickAnnotation', 'Deselect', 'DoubleClick', 'Framework', 'Hover', 'LegendClick', 'LegendDoubleClick', 'Relayout', 'Relayouting', 'Restyle', 'Redraw', 'Selected', 'Selecting', 'SliderChange', 'SliderEnd', 'SliderStart', 'SunburstClick', 'TreemapClick', 'Transitioning', 'TransitionInterrupted', 'Unhover', 'WebGlContextLost'];
+var updateEvents = ['plotly_restyle', 'plotly_redraw', 'plotly_relayout', 'plotly_relayouting', 'plotly_doubleclick', 'plotly_animated', 'plotly_sunburstclick', 'plotly_treemapclick']; // Check if a window is available since SSR (server-side rendering)
// breaks unnecessarily if you try to use it server-side.
var isBrowser = typeof window !== 'undefined';
```
File: /Users/morganmcguire/ML/marimo/frontend/public/android-chrome-192x192.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/public/android-chrome-512x512.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/public/apple-touch-icon.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/public/favicon-16x16.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/public/favicon-32x32.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/public/manifest.json
```json
{
"short_name": "Marimo",
"name": "A Marimo App",
"icons": [
{
"src": "favicon.ico",
"sizes": "48x48",
"type": "image/x-icon"
},
{
"src": "android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
```
File: /Users/morganmcguire/ML/marimo/frontend/public/site.webmanifest
```webmanifest
{
"background_color": "#ffffff",
"display": "standalone",
"icons": [
{
"sizes": "192x192",
"src": "/android-chrome-192x192.png",
"type": "image/png"
},
{
"sizes": "512x512",
"src": "/android-chrome-512x512.png",
"type": "image/png"
}
],
"name": "marimo",
"short_name": "marimo",
"theme_color": "#ffffff"
}
```
File: /Users/morganmcguire/ML/marimo/frontend/public/logo.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/src/__tests__/setup.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { afterEach } from "vitest";
import { cleanup } from "@testing-library/react";
import "@testing-library/jest-dom/vitest";
import "blob-polyfill";
// Cleanup after each test case (e.g., clearing jsdom)
afterEach(() => {
cleanup();
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/__tests__/components/editor/cell/CellStatus.test.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { formatElapsedTime } from "../../../../components/editor/cell/CellStatus";
import { describe, expect, test } from "vitest";
describe("formatElapsedTime", () => {
test("formats milliseconds correctly", () => {
expect(formatElapsedTime(500)).toBe("500ms");
expect(formatElapsedTime(50)).toBe("50ms");
});
test("formats seconds correctly", () => {
expect(formatElapsedTime(1500)).toBe("1.50s");
expect(formatElapsedTime(2340)).toBe("2.34s");
});
test("formats minutes and seconds correctly", () => {
expect(formatElapsedTime(60 * 1000)).toBe("1m0s");
expect(formatElapsedTime(90 * 1000)).toBe("1m30s");
expect(formatElapsedTime(89 * 1000)).toBe("1m29s");
expect(formatElapsedTime(91 * 1000)).toBe("1m31s");
expect(formatElapsedTime(150 * 1000)).toBe("2m30s");
expect(formatElapsedTime(151 * 1000)).toBe("2m31s");
});
test("handles null input", () => {
expect(formatElapsedTime(null)).toBe("");
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/assets/gradient.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/src/assets/noise.png
```png
[Binary file]
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/app-config-button.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Button as EditorButton } from "../editor/inputs/Inputs";
import { SettingsIcon } from "lucide-react";
import { UserConfigForm } from "./user-config-form";
import { Tooltip } from "../ui/tooltip";
import { Dialog, DialogTrigger, DialogContent } from "../ui/dialog";
import { AppConfigForm } from "@/components/app-config/app-config-form";
import { useAtom } from "jotai";
import { Button } from "../ui/button";
import { settingDialogAtom } from "./state";
interface Props {
showAppConfig?: boolean;
}
export const ConfigButton: React.FC<Props> = ({ showAppConfig = true }) => {
const [settingDialog, setSettingDialog] = useAtom(settingDialogAtom);
const button = (
<EditorButton
aria-label="Config"
data-testid="app-config-button"
shape="circle"
size="small"
className="h-[27px] w-[27px]"
color="hint-green"
>
<Tooltip content="Settings">
<SettingsIcon strokeWidth={1.8} />
</Tooltip>
</EditorButton>
);
const userSettingsDialog = (
<DialogContent className="w-[80vw] h-[70vh] overflow-hidden sm:max-w-5xl top-[15vh] p-0">
<UserConfigForm />
</DialogContent>
);
if (!showAppConfig) {
return (
<Dialog open={settingDialog} onOpenChange={setSettingDialog}>
<DialogTrigger>{button}</DialogTrigger>
{userSettingsDialog}
</Dialog>
);
}
return (
<>
<Popover>
<PopoverTrigger asChild={true}>{button}</PopoverTrigger>
<PopoverContent
className="w-80 overflow-auto"
align="end"
side="bottom"
// prevent focus outside to hack around a bug in which
// interacting with buttons closes the popover ...
onFocusOutside={(evt) => evt.preventDefault()}
>
<AppConfigForm />
<div className="h-px bg-border my-2" />
<Button
onClick={() => setSettingDialog(true)}
variant="link"
className="px-0"
>
<SettingsIcon strokeWidth={1.8} className="w-4 h-4 mr-2" />
User settings
</Button>
</PopoverContent>
</Popover>
<Dialog open={settingDialog} onOpenChange={setSettingDialog}>
{userSettingsDialog}
</Dialog>
</>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/app-config-form.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import {
type AppConfig,
AppConfigSchema,
AppTitleSchema,
} from "../../core/config/config-schema";
import { getAppWidths } from "@/core/config/widths";
import { Input } from "../ui/input";
import { NativeSelect } from "../ui/native-select";
import { useAppConfig } from "@/core/config/config";
import { saveAppConfig } from "@/core/network/requests";
import { SettingTitle, SettingDescription } from "./common";
import { useEffect } from "react";
import { Checkbox } from "../ui/checkbox";
import { arrayToggle } from "@/utils/arrays";
import { Kbd } from "../ui/kbd";
export const AppConfigForm: React.FC = () => {
const [config, setConfig] = useAppConfig();
// Create form
const form = useForm<AppConfig>({
resolver: zodResolver(AppConfigSchema),
defaultValues: config,
});
const onSubmit = async (values: AppConfig) => {
await saveAppConfig({ config: values })
.then(() => {
setConfig(values);
})
.catch(() => {
setConfig(values);
});
};
// When width is changed, dispatch a resize event so widgets know to resize
useEffect(() => {
window.dispatchEvent(new Event("resize"));
}, [config.width]);
return (
<Form {...form}>
<form
onChange={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div>
<SettingTitle>Application Config</SettingTitle>
<SettingDescription>
Settings applied to this notebook
</SettingDescription>
</div>
<FormField
control={form.control}
name="width"
render={({ field }) => (
<FormItem
className={"flex flex-row items-center space-x-1 space-y-0"}
>
<FormLabel>Width</FormLabel>
<FormControl>
<NativeSelect
data-testid="app-width-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{getAppWidths().map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="app_title"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className="flex flex-row items-center space-x-1 space-y-0">
<FormLabel>App title</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
onChange={(e) => {
field.onChange(e.target.value);
if (AppTitleSchema.safeParse(e.target.value).success) {
document.title = e.target.value;
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
<FormDescription>
The application title is put in the title tag in the HTML code
and typically displayed in the title bar of the browser window.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="css_file"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className="flex flex-row items-center space-x-1 space-y-0">
<FormLabel className="flex-shrink-0">Custom CSS</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder="custom.css"
onChange={(e) => {
field.onChange(e.target.value);
if (AppTitleSchema.safeParse(e.target.value).success) {
document.title = e.target.value;
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
<FormDescription>
A filepath to a custom css file to be injected into the
notebook.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="html_head_file"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className="flex flex-row items-center space-x-1 space-y-0">
<FormLabel className="flex-shrink-0">HTML Head</FormLabel>
<FormControl>
<Input
{...field}
value={field.value ?? ""}
placeholder="head.html"
onChange={(e) => {
field.onChange(e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
<FormDescription>
A filepath to an HTML file to be injected into the{" "}
<Kbd className="inline">{"<head/>"}</Kbd> section of the
notebook. Use this to add analytics, custom fonts, meta tags, or
external scripts.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="auto_download"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<div className="text-base font-bold text-muted-foreground">
Auto-download
</div>
<FormItem className="flex flex-col gap-2">
<FormControl>
<div className="flex gap-4">
<div className="flex items-center space-x-2">
<Checkbox
id="html-checkbox"
checked={field.value.includes("html")}
onCheckedChange={() => {
field.onChange(arrayToggle(field.value, "html"));
}}
/>
<FormLabel htmlFor="html-checkbox">HTML</FormLabel>
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="ipynb-checkbox"
checked={field.value.includes("ipynb")}
onCheckedChange={() => {
field.onChange(arrayToggle(field.value, "ipynb"));
}}
/>
<FormLabel htmlFor="ipynb-checkbox">IPYNB</FormLabel>
</div>
{/* Disable markdown until we save outputs in the exported markdown */}
{/* <div className="flex items-center space-x-2">
<Checkbox
id="markdown-checkbox"
checked={field.value.includes("markdown")}
onCheckedChange={() => {
field.onChange(arrayToggle(field.value, "markdown"));
}}
/>
<label
htmlFor="markdown-checkbox"
className="cursor-pointer"
>
Markdown
</label>
</div> */}
</div>
</FormControl>
<FormMessage />
</FormItem>
<FormDescription>
When enabled, marimo will periodically save this notebook in
your selected formats (HTML, IPYNB) to a folder named{" "}
<Kbd className="inline">__marimo__</Kbd> next to your notebook
file.
</FormDescription>
</div>
)}
/>
</form>
</Form>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/common.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { cn } from "@/utils/cn";
import type { HTMLProps, PropsWithChildren } from "react";
export const SettingTitle: React.FC<PropsWithChildren> = ({ children }) => {
return (
<div className="text-md font-semibold text-muted-foreground uppercase tracking-wide mb-1">
{children}
</div>
);
};
export const SettingSubtitle: React.FC<HTMLProps<HTMLDivElement>> = ({
children,
className,
...props
}) => {
return (
<div
{...props}
className={cn(
"text-sm font-semibold underline-offset-2 text-accent-foreground uppercase tracking-wide",
className,
)}
>
{children}
</div>
);
};
export const SettingDescription: React.FC<PropsWithChildren> = ({
children,
}) => {
return <p className="text-sm text-muted-foreground">{children}</p>;
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/constants.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
export const KNOWN_AI_MODELS = [
// Anthropic
"claude-3-5-sonnet-20241022",
"claude-3-opus",
"claude-3.5-haiku",
"claude-3.5-sonnet",
// DeepSeek
"deepseek-v3",
// Google
"gemini-2.0-flash-exp",
"gemini-2.0-flash-thinking-exp",
"gemini-exp-1206",
// OpenAI
"gpt-3.5-turbo",
"gpt-4",
"gpt-4-turbo-2024-04-09",
"gpt-4o",
"gpt-4o-mini",
"o1",
"o1-mini",
"o1-preview",
] as const;
```
File: /Users/morganmcguire/ML/marimo/frontend/public/circle-x.ico
```ico
00®(0` ..Ë''€%%€&&‹&&‹&&€''›%%›&&€((›%%‹&&‹''‹&&‹((◊&&‹''‹&&›&&‹%%‹&&‹((€''‹&&€&&‹ˇˇˇ  



  

    



    

  



  ˇˇˇˇˇˇˇˇˇˇˇˇˇˇÄˇˇˇ˛ˇˇ¯ˇˇ‡ˇˇ¿ˇˇÄˇˇˇ˛¸?¯¯‡‡¿¿¿¿¿¿¿¿¿¿¿¿¿¿‡‡¯¯¸?˛ˇˇˇÄˇˇ¿ˇˇ‡ˇˇ¯ˇˇ˛ˇˇˇÄˇˇˇˇˇˇˇˇˇˇˇˇˇˇ
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/state.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { atom, useSetAtom } from "jotai";
import {
activeUserConfigCategoryAtom,
type SettingCategoryId,
} from "./user-config-form";
export const settingDialogAtom = atom<boolean>(false);
export function useOpenSettingsToTab() {
const setActiveCategory = useSetAtom(activeUserConfigCategoryAtom);
const setSettingsDialog = useSetAtom(settingDialogAtom);
const handleClick = (tab: SettingCategoryId) => {
setActiveCategory(tab);
setSettingsDialog(true);
};
return { handleClick };
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/buttons/clear-button.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { cn } from "@/utils/cn";
interface ClearButtonProps {
className?: string;
dataTestId?: string;
onClick: () => void;
}
export const ClearButton: React.FC<ClearButtonProps> = (props) => (
<button
type="button"
data-testid={props.dataTestId}
className={cn(
"text-xs font-semibold text-accent-foreground",
props.className,
)}
onClick={props.onClick}
>
Clear
</button>
);
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/audio/audio-recorder.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { cn } from "@/utils/cn";
import React from "react";
import { Button } from "../ui/button";
import { CircleIcon, SquareIcon } from "lucide-react";
import type { RecordingStatus } from "@/hooks/useAudioRecorder";
interface AudioRecorderProps {
onStart: () => void;
onStop: () => void;
onPause: () => void;
status: RecordingStatus;
time?: string;
}
export const AudioRecorder: React.FC<AudioRecorderProps> = ({
onStart,
onStop,
onPause,
status,
time,
}) => {
return (
<div className="flex items-center gap-3">
{status === "stopped" && (
<Button
data-testid="audio-recorder-start"
variant="secondary"
onClick={onStart}
className="w-[50px]"
>
<CircleIcon
className={cn("w-6 h-6 border border-input rounded-full")}
strokeWidth={1.5}
fill="var(--red-9)"
/>
</Button>
)}
{status === "recording" && (
<Button
data-testid="audio-recorder-pause"
variant="secondary"
onClick={onStop}
className="w-[50px]"
>
<SquareIcon
className="w-5 h-5 rounded-sm"
fill="var(--red-9)"
strokeWidth={1.5}
/>
<CircleIcon
className={cn("w-6 h-6 absolute opacity-20 animate-ping")}
fill="var(--red-9)"
style={{ animationDuration: "1.5s" }}
strokeWidth={0}
/>
</Button>
)}
{time && <span className="text-sm font-bold">{time}s</span>}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/buttons/undo-button.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { useEventListener } from "@/hooks/useEventListener";
import { MinimalHotkeys } from "../shortcuts/renderShortcut";
import { Button, type ButtonProps } from "../ui/button";
/* Copyright 2024 Marimo. All rights reserved. */
interface UndoButtonProps extends Omit<ButtonProps, "onClick"> {
onClick?: (event: Pick<Event, "preventDefault" | "stopPropagation">) => void;
}
export const UndoButton = (props: UndoButtonProps) => {
// Add ctrl-z or meta-z event listener
useEventListener(
window,
"keydown",
(event) => {
if ((event.ctrlKey || event.metaKey) && event.key === "z") {
event.preventDefault();
event.stopPropagation();
props.onClick?.(event);
}
},
{
capture: true,
},
);
const children = props.children ?? "Undo";
return (
<Button data-testid="undo-button" size="sm" variant="outline" {...props}>
{children} <MinimalHotkeys className="ml-2" shortcut="cmd-z" />
</Button>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/charts/chart-skeleton.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React from "react";
interface Props {
seed: string;
width: number;
height: number;
}
// Simple hash function to convert a string to a number
const hashString = (str: string) => {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = (hash << 5) - hash + char;
hash = Math.trunc(hash); // Convert to 32bit integer
}
return hash;
};
// Utility function to generate deterministic random heights based on a seed
const generateHeights = (numBars: number, maxHeight: number, seed: string) => {
const heights = [];
let randomSeed = hashString(seed);
for (let i = 0; i < numBars; i++) {
randomSeed = (randomSeed * 9301 + 49_297) % 233_280;
const random = randomSeed / 233_280;
const height = Math.abs(Math.floor(random * maxHeight));
heights.push(height);
}
return heights;
};
export const ChartSkeleton: React.FC<Props> = ({ seed, width, height }) => {
const numBars = 9;
const barWidth = width / numBars;
const heights = generateHeights(numBars, height - 15, seed);
return (
<div className="flex items-end gap-[1px] pb-2" style={{ width, height }}>
{heights.map((barHeight, index) => (
<div
key={index}
className="bg-[var(--slate-5)] animate-pulse"
style={{ width: barWidth - 2, height: barHeight }}
/>
))}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/chat/chat-panel.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { useAtom } from "jotai";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ClockIcon, Loader2, PlusIcon } from "lucide-react";
import { chatStateAtom, activeChatAtom, type Chat } from "@/core/ai/state";
import { useState, useRef, useEffect } from "react";
import { generateUUID } from "@/utils/uuid";
import { useChat } from "ai/react";
import { PromptInput } from "../editor/ai/add-cell-with-ai";
import type { ReactCodeMirrorRef } from "@uiw/react-codemirror";
import { Tooltip } from "../ui/tooltip";
import { asURL } from "@/utils/url";
import { API } from "@/core/network/api";
import { cn } from "@/utils/cn";
import { MarkdownRenderer } from "./markdown-renderer";
import { Logger } from "@/utils/Logger";
import { getCodes } from "@/core/codemirror/copilot/getCodes";
import { getAICompletionBody } from "../editor/ai/completion-utils";
import { addMessageToChat } from "@/core/ai/chat-utils";
import { ErrorBanner } from "@/plugins/impl/common/error-banner";
import { useTheme } from "@/theme/useTheme";
export const ChatPanel = () => {
const [chatState, setChatState] = useAtom(chatStateAtom);
const [activeChat, setActiveChat] = useAtom(activeChatAtom);
const [completionBody, setCompletionBody] = useState<object>({});
const [newThreadInput, setNewThreadInput] = useState("");
const newThreadInputRef = useRef<ReactCodeMirrorRef>(null);
const newMessageInputRef = useRef<ReactCodeMirrorRef>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const { theme } = useTheme();
const {
messages,
input,
setInput: setInputInternal,
setMessages,
append,
handleSubmit,
error,
isLoading,
reload,
} = useChat({
keepLastMessageOnError: true,
api: asURL("api/ai/chat").toString(),
headers: API.headers(),
body: {
...completionBody,
includeOtherCode: getCodes(""),
},
streamProtocol: "text",
onFinish: (message) => {
if (!chatState.activeChatId) {
Logger.warn("No active chat");
return;
}
setChatState((prev) =>
addMessageToChat(prev, prev.activeChatId, "assistant", message.content),
);
},
onError: (error) => {
Logger.error("An error occurred:", error);
},
onResponse: (response) => {
Logger.debug("Received HTTP response from server:", response);
},
});
const setInput = (newValue: string) => {
setInputInternal(newValue);
const messagesConcat = messages.map((m) => m.content).join("\n");
setCompletionBody(getAICompletionBody(`${messagesConcat}\n\n${newValue}`));
};
const lastMessageText = messages.at(-1)?.content;
useEffect(() => {
if (isLoading) {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
}
}, [messages, isLoading, lastMessageText]);
const createNewThread = (initialMessage: string) => {
const newChat: Chat = {
id: generateUUID(),
title: `${initialMessage.slice(0, 30)}...`,
messages: [
{
role: "user",
content: initialMessage,
timestamp: Date.now(),
},
],
createdAt: Date.now(),
updatedAt: Date.now(),
};
setChatState((prev) => ({
chats: [...prev.chats, newChat],
activeChatId: newChat.id,
}));
const nextCompletionBody = getAICompletionBody(initialMessage);
setCompletionBody(nextCompletionBody);
setMessages([]);
setInput("");
append(
{
role: "user",
content: initialMessage,
},
{
body: {
...nextCompletionBody,
includeOtherCode: getCodes(""),
},
},
);
};
return (
<div className="flex flex-col h-[calc(100%-53px)]">
<div className="flex border-b px-2 py-1 justify-between flex-shrink-0">
<Tooltip content="New chat">
<Button
variant="text"
size="icon"
onClick={() => {
setActiveChat(null);
setMessages([]);
setInput("");
setNewThreadInput("");
}}
>
<PlusIcon className="h-4 w-4" />
</Button>
</Tooltip>
<Popover>
<Tooltip content="Previous chats">
<PopoverTrigger asChild={true}>
<Button variant="text" size="icon">
<ClockIcon className="h-4 w-4" />
</Button>
</PopoverTrigger>
</Tooltip>
<PopoverContent className="w-[520px] p-0" align="start" side="right">
<ScrollArea className="h-[500px] p-4">
<div className="space-y-4">
{chatState.chats.map((chat) => (
<div
key={chat.id}
className={cn(
"p-3 rounded-md cursor-pointer hover:bg-accent",
chat.id === activeChat?.id && "bg-accent",
)}
onClick={() => {
setActiveChat(chat.id);
setMessages(
chat.messages.map(({ role, content, timestamp }) => ({
role,
content,
id: timestamp.toString(),
})),
);
}}
>
<div className="font-medium">{chat.title}</div>
<div className="text-sm text-muted-foreground">
{new Date(chat.updatedAt).toLocaleString()}
</div>
</div>
))}
</div>
</ScrollArea>
</PopoverContent>
</Popover>
</div>
<div className="flex-1 px-3 bg-[var(--slate-1)] gap-4 py-3 flex flex-col overflow-y-auto">
{(!messages || messages.length === 0) && (
<div className="flex rounded-md border px-1 bg-background">
<PromptInput
key="new-thread-input"
value={newThreadInput}
placeholder="Ask anything, @ to include context"
theme={theme}
onClose={() => newThreadInputRef.current?.editor?.blur()}
onChange={setNewThreadInput}
onSubmit={() =>
newThreadInput.trim() && createNewThread(newThreadInput.trim())
}
/>
</div>
)}
{messages.map((message, idx) => (
<div
key={idx}
className={cn(
"flex",
message.role === "user" ? "justify-end" : "justify-start",
)}
>
{message.role === "user" ? (
<div className="w-[95%] bg-background border p-1 rounded-sm">
<PromptInput
key={message.id}
value={message.content}
theme={theme}
placeholder="Type your message..."
onChange={() => {
// noop
}}
onSubmit={(e, newValue) => {
if (!newValue.trim()) {
return;
}
// Remove all messages from here to the end
setMessages((messages) => messages.slice(0, idx));
setCompletionBody(getAICompletionBody(newValue));
append({
role: "user",
content: newValue,
});
if (chatState.activeChatId) {
setChatState((prev) =>
addMessageToChat(
prev,
chatState.activeChatId,
"user",
newValue,
),
);
}
}}
onClose={() => {
// noop
}}
/>
</div>
) : (
<div className="w-[95%]">
<MarkdownRenderer content={message.content} />
</div>
)}
</div>
))}
{isLoading && (
<div className="flex justify-center py-4">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
)}
{error && (
<div className="flex items-center justify-center space-x-2 mb-4">
<ErrorBanner error={error} />
<Button variant="outline" size="sm" onClick={() => reload()}>
Retry
</Button>
</div>
)}
<div ref={messagesEndRef} />
</div>
{messages && messages.length > 0 && (
<div className="px-2 py-3 border-t relative flex-shrink-0 min-h-[80px]">
{isLoading && (
<div className="flex justify-center mb-2 absolute -top-10 left-0 right-0">
<Button
variant="secondary"
size="xs"
onClick={() => stop()}
className="text-muted-foreground hover:text-foreground"
>
Cancel
</Button>
</div>
)}
<PromptInput
value={input}
onChange={setInput}
onSubmit={(e, newValue) => {
if (!newValue.trim()) {
return;
}
handleSubmit(e);
if (chatState.activeChatId) {
setChatState((prev) =>
addMessageToChat(
prev,
chatState.activeChatId,
"user",
newValue,
),
);
}
}}
onClose={() => newMessageInputRef.current?.editor?.blur()}
theme={theme}
placeholder="Type your message..."
/>
</div>
)}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/app-config/user-config-form.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { SettingSubtitle } from "./common";
import React, { useRef } from "react";
import { type FieldPath, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "@/components/ui/button";
import { NativeSelect } from "@/components/ui/native-select";
import { NumberField } from "@/components/ui/number-field";
import { Kbd } from "@/components/ui/kbd";
import { CopilotConfig } from "@/core/codemirror/copilot/copilot-config";
import { KEYMAP_PRESETS } from "@/core/codemirror/keymaps/keymaps";
import { configOverridesAtom, useUserConfig } from "@/core/config/config";
import {
UserConfigSchema,
PackageManagerNames,
type UserConfig,
} from "@/core/config/config-schema";
import { getAppWidths } from "@/core/config/widths";
import { saveUserConfig } from "@/core/network/requests";
import { isWasm } from "@/core/wasm/utils";
import { THEMES } from "@/theme/useTheme";
import { keyboardShortcutsAtom } from "../editor/controls/keyboard-shortcuts";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
EditIcon,
MonitorIcon,
PackageIcon,
CpuIcon,
BrainIcon,
FlaskConicalIcon,
FolderCog2,
} from "lucide-react";
import { cn } from "@/utils/cn";
import { KNOWN_AI_MODELS } from "./constants";
import { Textarea } from "../ui/textarea";
import { get } from "lodash-es";
import { Tooltip } from "../ui/tooltip";
const formItemClasses = "flex flex-row items-center space-x-1 space-y-0";
const categories = [
{
id: "editor",
label: "Editor",
Icon: EditIcon,
className: "bg-[var(--blue-4)]",
},
{
id: "display",
label: "Display",
Icon: MonitorIcon,
className: "bg-[var(--grass-4)]",
},
{
id: "packageManagement",
label: "Package Management",
Icon: PackageIcon,
className: "bg-[var(--red-4)]",
},
{
id: "runtime",
label: "Runtime",
Icon: CpuIcon,
className: "bg-[var(--amber-4)]",
},
{
id: "ai",
label: "AI",
Icon: BrainIcon,
className: "bg-[linear-gradient(45deg,var(--purple-5),var(--cyan-5))]",
},
{
id: "labs",
label: "Labs",
Icon: FlaskConicalIcon,
className: "bg-[var(--slate-4)]",
},
] as const;
export type SettingCategoryId = (typeof categories)[number]["id"];
export const activeUserConfigCategoryAtom = atom<SettingCategoryId>(
categories[0].id,
);
export const UserConfigForm: React.FC = () => {
const [config, setConfig] = useUserConfig();
const formElement = useRef<HTMLFormElement>(null);
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
const [activeCategory, setActiveCategory] = useAtom(
activeUserConfigCategoryAtom,
);
// Create form
const form = useForm<UserConfig>({
resolver: zodResolver(UserConfigSchema),
defaultValues: config,
});
const onSubmit = async (values: UserConfig) => {
await saveUserConfig({ config: values }).then(() => {
setConfig(values);
});
};
const isWasmRuntime = isWasm();
const renderCopilotProvider = () => {
const copilot = form.getValues("completion.copilot");
if (copilot === false) {
return null;
}
if (copilot === "codeium") {
return (
<>
<p className="text-sm text-muted-secondary">
To get a Codeium API key, follow{" "}
<a
className="text-link hover:underline"
href="https://docs.marimo.io/guides/editor_features/ai_completion.html#codeium-copilot"
target="_blank"
rel="noreferrer"
>
these instructions
</a>
.
</p>
<FormField
control={form.control}
name="completion.codeium_api_key"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel>API Key</FormLabel>
<FormControl>
<Input
data-testid="codeium-api-key-input"
className="m-0 inline-flex"
placeholder="key"
{...field}
value={field.value || ""}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="completion.codeium_api_key"
/>
</FormItem>
)}
/>
</>
);
}
if (copilot === "github") {
return <CopilotConfig />;
}
};
const renderBody = () => {
switch (activeCategory) {
case "editor":
return (
<>
<SettingGroup title="Autosave">
<FormField
control={form.control}
name="save.autosave"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
Autosave enabled
</FormLabel>
<FormControl>
<Checkbox
data-testid="autosave-checkbox"
checked={field.value === "after_delay"}
disabled={field.disabled}
onCheckedChange={(checked) => {
field.onChange(checked ? "after_delay" : "off");
}}
/>
</FormControl>
<FormMessage />
<IsOverridden userConfig={config} name="save.autosave" />
</FormItem>
)}
/>
<FormField
control={form.control}
name="save.autosave_delay"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel>Autosave delay (seconds)</FormLabel>
<FormControl>
<NumberField
data-testid="autosave-delay-input"
className="m-0 w-24"
isDisabled={
form.getValues("save.autosave") !== "after_delay"
}
{...field}
value={field.value / 1000}
minValue={1}
onChange={(value) => {
field.onChange(value * 1000);
if (!Number.isNaN(value)) {
onSubmit(form.getValues());
}
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="save.autosave_delay"
/>
</FormItem>
)}
/>
</SettingGroup>
<SettingGroup title="Formatting">
<FormField
control={form.control}
name="save.format_on_save"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
Format on save
</FormLabel>
<FormControl>
<Checkbox
data-testid="format-on-save-checkbox"
checked={field.value}
disabled={field.disabled}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="save.format_on_save"
/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="formatting.line_length"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Line length</FormLabel>
<FormControl>
<NumberField
data-testid="line-length-input"
className="m-0 w-24"
{...field}
value={field.value}
minValue={1}
maxValue={1000}
onChange={(value) => {
// Ignore NaN
field.onChange(value);
if (!Number.isNaN(value)) {
onSubmit(form.getValues());
}
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="formatting.line_length"
/>
</FormItem>
<FormDescription>
Maximum line length when formatting code.
</FormDescription>
</div>
)}
/>
</SettingGroup>
<SettingGroup title="Autocomplete">
<FormField
control={form.control}
name="completion.activate_on_typing"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
Autocomplete
</FormLabel>
<FormControl>
<Checkbox
data-testid="autocomplete-checkbox"
checked={field.value}
disabled={field.disabled}
onCheckedChange={(checked) => {
field.onChange(Boolean(checked));
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="completion.activate_on_typing"
/>
</FormItem>
<FormDescription>
When unchecked, code completion is still available through
a hotkey.
</FormDescription>
<div>
<Button
variant="link"
className="mb-0 px-0"
type="button"
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
setActiveCategory("ai");
}}
>
Edit AI autocomplete
</Button>
</div>
</div>
)}
/>
</SettingGroup>
<SettingGroup title="Keymap">
<FormField
control={form.control}
name="keymap.preset"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Keymap</FormLabel>
<FormControl>
<NativeSelect
data-testid="keymap-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{KEYMAP_PRESETS.map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden userConfig={config} name="keymap.preset" />
</FormItem>
<div>
<Button
variant="link"
className="mb-0 px-0"
type="button"
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
setKeyboardShortcutsOpen(true);
}}
>
Edit Keyboard Shortcuts
</Button>
</div>
</div>
)}
/>
</SettingGroup>
</>
);
case "display":
return (
<>
<SettingGroup title="Display">
<FormField
control={form.control}
name="display.default_width"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Default width</FormLabel>
<FormControl>
<NativeSelect
data-testid="user-config-width-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{getAppWidths().map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="display.default_width"
/>
</FormItem>
<FormDescription>
The default app width for new notebooks; overridden by
"width" in the application config.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="display.theme"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Theme</FormLabel>
<FormControl>
<NativeSelect
data-testid="theme-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{THEMES.map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden userConfig={config} name="display.theme" />
</FormItem>
<FormDescription>
This theme will be applied to the user's configuration; it
does not affect theme when sharing the notebook.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="display.code_editor_font_size"
render={({ field }) => (
<FormItem className={formItemClasses}>
<FormLabel>Code editor font size (px)</FormLabel>
<FormControl>
<span className="inline-flex mr-2">
<NumberField
data-testid="code-editor-font-size-input"
className="m-0 w-24"
{...field}
value={field.value}
minValue={8}
maxValue={32}
onChange={(value) => {
field.onChange(value);
onSubmit(form.getValues());
}}
/>
</span>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="display.code_editor_font_size"
/>
</FormItem>
)}
/>
</SettingGroup>
<SettingGroup title="Outputs">
<FormField
control={form.control}
name="display.cell_output"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Cell output area</FormLabel>
<FormControl>
<NativeSelect
data-testid="cell-output-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{["above", "below"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="display.cell_output"
/>
</FormItem>
<FormDescription>
Where to display cell's output.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="display.dataframes"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Dataframe viewer</FormLabel>
<FormControl>
<NativeSelect
data-testid="display-dataframes-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{["rich", "plain"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="display.dataframes"
/>
</FormItem>
<FormDescription>
Whether to use marimo's rich dataframe viewer or a plain
HTML table. This requires restarting your notebook to take
effect.
</FormDescription>
</div>
)}
/>
</SettingGroup>
</>
);
case "packageManagement":
return (
<SettingGroup title="Package Management">
<FormField
control={form.control}
disabled={isWasmRuntime}
name="package_management.manager"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Manager</FormLabel>
<FormControl>
<NativeSelect
data-testid="package-manager-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={field.disabled}
className="inline-flex mr-2"
>
{PackageManagerNames.map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="package_management.manager"
/>
</FormItem>
<FormDescription>
When marimo comes across a module that is not installed, you
will be prompted to install it using your preferred package
manager. Learn more in the{" "}
<a
className="text-link hover:underline"
href="https://docs.marimo.io/guides/editor_features/package_management.html"
target="_blank"
rel="noreferrer"
>
docs
</a>
.
<br />
<br />
Running marimo in a{" "}
<a
className="text-link hover:underline"
href="https://docs.marimo.io/guides/editor_features/package_management.html#running-marimo-in-a-sandbox-environment-uv-only"
target="_blank"
rel="noreferrer"
>
sandboxed environment
</a>{" "}
is only supported by <Kbd className="inline">uv</Kbd>
</FormDescription>
</div>
)}
/>
</SettingGroup>
);
case "runtime":
return (
<SettingGroup title="Runtime configuration">
<FormField
control={form.control}
name="runtime.auto_instantiate"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
Autorun on startup
</FormLabel>
<FormControl>
<Checkbox
data-testid="auto-instantiate-checkbox"
disabled={field.disabled}
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="runtime.auto_instantiate"
/>
</FormItem>
<FormDescription>
Whether to automatically run all cells on startup.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="runtime.on_cell_change"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
On cell change
</FormLabel>
<FormControl>
<NativeSelect
data-testid="on-cell-change-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
className="inline-flex mr-2"
>
{["lazy", "autorun"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="runtime.on_cell_change"
/>
</FormItem>
<FormDescription>
Whether marimo should automatically run cells or just mark
them as stale. If "autorun", marimo will automatically run
affected cells when a cell is run or a UI element is
interacted with; if "lazy", marimo will mark affected cells
as stale but won't re-run them.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="runtime.auto_reload"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
On module change
</FormLabel>
<FormControl>
<NativeSelect
data-testid="auto-reload-select"
onChange={(e) => field.onChange(e.target.value)}
value={field.value}
disabled={isWasmRuntime}
className="inline-flex mr-2"
>
{["off", "lazy", "autorun"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="runtime.auto_reload"
/>
</FormItem>
<FormDescription>
Whether marimo should automatically reload modules before
executing cells. If "lazy", marimo will mark cells affected
by module modifications as stale; if "autorun", affected
cells will be automatically re-run.
</FormDescription>
</div>
)}
/>
<FormDescription>
Learn more in the{" "}
<a
className="text-link hover:underline"
href="https://docs.marimo.io/guides/reactivity/#configuring-how-marimo-runs-cells"
target="_blank"
rel="noreferrer"
>
docs
</a>
.
</FormDescription>
</SettingGroup>
);
case "ai":
return (
<>
<SettingGroup title="AI Code Completion">
<p className="text-sm text-muted-secondary">
You may use GitHub Copilot or Codeium for AI code completion.
</p>
<FormField
control={form.control}
name="completion.copilot"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Provider</FormLabel>
<FormControl>
<NativeSelect
data-testid="copilot-select"
onChange={(e) => {
if (e.target.value === "none") {
field.onChange(false);
} else {
field.onChange(e.target.value);
}
}}
value={
field.value === true
? "github"
: field.value === false
? "none"
: field.value
}
disabled={field.disabled}
className="inline-flex mr-2"
>
{["none", "github", "codeium"].map((option) => (
<option value={option} key={option}>
{option}
</option>
))}
</NativeSelect>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="completion.copilot"
/>
</FormItem>
</div>
)}
/>
{renderCopilotProvider()}
</SettingGroup>
<SettingGroup title="AI Keys">
<FormField
control={form.control}
name="ai.open_ai.api_key"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>OpenAI API Key</FormLabel>
<FormControl>
<Input
data-testid="ai-openai-api-key-input"
className="m-0 inline-flex"
placeholder="sk-proj..."
{...field}
onChange={(e) => {
const value = e.target.value;
// Don't allow *
if (!value.includes("*")) {
field.onChange(value);
}
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="ai.open_ai.api_key"
/>
</FormItem>
<FormDescription>
Your OpenAI API key from{" "}
<a
className="text-link hover:underline"
href="https://platform.openai.com/account/api-keys"
target="_blank"
rel="noreferrer"
>
platform.openai.com
</a>
.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="ai.anthropic.api_key"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Anthropic API Key</FormLabel>
<FormControl>
<Input
data-testid="ai-anthropic-api-key-input"
className="m-0 inline-flex"
placeholder="sk-ant..."
{...field}
onChange={(e) => {
const value = e.target.value;
// Don't allow *
if (!value.includes("*")) {
field.onChange(value);
}
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="ai.anthropic.api_key"
/>
</FormItem>
<FormDescription>
Your Anthropic API key from{" "}
<a
className="text-link hover:underline"
href="https://console.anthropic.com/settings/keys"
target="_blank"
rel="noreferrer"
>
console.anthropic.com
</a>
.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="ai.google.api_key"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Google AI API Key</FormLabel>
<FormControl>
<Input
data-testid="ai-google-api-key-input"
className="m-0 inline-flex"
placeholder="AI..."
{...field}
onChange={(e) => {
const value = e.target.value;
// Don't allow *
if (!value.includes("*")) {
field.onChange(value);
}
}}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="ai.google.api_key"
/>
</FormItem>
<FormDescription>
Your Google AI API key from{" "}
<a
className="text-link hover:underline"
href="https://aistudio.google.com/app/apikey"
target="_blank"
rel="noreferrer"
>
aistudio.google.com
</a>
.
</FormDescription>
</div>
)}
/>
</SettingGroup>
<SettingGroup title="AI Assist">
<p className="text-sm text-muted-secondary">
Add an API key to <Kbd className="inline">marimo.toml</Kbd> to
activate marimo's AI assistant; see{" "}
<a
className="text-link hover:underline"
href="https://docs.marimo.io/guides/editor_features/ai_completion.html"
target="_blank"
rel="noreferrer"
>
docs
</a>{" "}
for more info.
</p>
<FormField
control={form.control}
disabled={isWasmRuntime}
name="ai.open_ai.base_url"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Base URL</FormLabel>
<FormControl>
<Input
data-testid="ai-base-url-input"
className="m-0 inline-flex"
placeholder="https://api.openai.com/v1"
{...field}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="ai.open_ai.base_url"
/>
</FormItem>
<FormDescription>
This URL can be any OpenAI-compatible API endpoint.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
disabled={isWasmRuntime}
name="ai.open_ai.model"
render={({ field }) => (
<div className="flex flex-col space-y-1">
<FormItem className={formItemClasses}>
<FormLabel>Model</FormLabel>
<FormControl>
<Input
list="ai-model-datalist"
data-testid="ai-model-input"
className="m-0 inline-flex"
placeholder="gpt-4-turbo"
{...field}
/>
</FormControl>
<FormMessage />
<IsOverridden
userConfig={config}
name="ai.open_ai.model"
/>
</FormItem>
<datalist id="ai-model-datalist">
{KNOWN_AI_MODELS.map((model) => (
<option value={model} key={model}>
{model}
</option>
))}
</datalist>
<FormDescription>
If the model starts with "claude-", we will use your
Anthropic API key. If the model starts with "gemini-", we
will use your Google AI API key. Otherwise, we will use
your OpenAI API key.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="ai.rules"
render={({ field }) => (
<div className="flex flex-col">
<FormItem>
<FormLabel>Custom Rules</FormLabel>
<FormControl>
<Textarea
data-testid="ai-rules-input"
className="m-0 inline-flex w-full h-32 p-2 text-sm"
placeholder="e.g. Always use type hints; prefer polars over pandas"
{...field}
value={field.value}
/>
</FormControl>
<FormMessage />
<IsOverridden userConfig={config} name="ai.rules" />
</FormItem>
<FormDescription>
Custom rules to include in all AI completion prompts.
</FormDescription>
</div>
)}
/>
</SettingGroup>
</>
);
case "labs":
return (
<SettingGroup title="Experimental Features">
<p className="text-sm text-muted-secondary mb-4">
⚠️ These features are experimental and may require restarting your
notebook to take effect.
</p>
<FormField
control={form.control}
name="experimental.chat_sidebar"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">Chat sidebar</FormLabel>
<FormControl>
<Checkbox
data-testid="chat-sidebar-checkbox"
checked={field.value === true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
<FormDescription>
Enable experimental chat sidebar to ask questions with an AI
assistant.
</FormDescription>
</div>
)}
/>
<FormField
control={form.control}
name="experimental.inline_ai_tooltip"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
AI Edit Tooltip
</FormLabel>
<FormControl>
<Checkbox
data-testid="inline-ai-checkbox"
checked={field.value === true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
<FormDescription>
Enable experimental "Edit with AI" tooltip when selecting
code.
</FormDescription>
</div>
)}
/>
{!isWasm() && (
<FormField
control={form.control}
name="experimental.rtc"
render={({ field }) => (
<div className="flex flex-col gap-y-1">
<FormItem className={formItemClasses}>
<FormLabel className="font-normal">
Real-time Collaboration
</FormLabel>
<FormControl>
<Checkbox
data-testid="rtc-checkbox"
checked={field.value === true}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
<FormDescription>
Enable experimental real-time collaboration to allow
editing cell inputs by multiple users. Requires refreshing
the page to take effect.
</FormDescription>
</div>
)}
/>
)}
</SettingGroup>
);
}
};
const configMessage = (
<p className="text-muted-secondary">
User configuration is stored in <Kbd className="inline">marimo.toml</Kbd>
<br />
Run <Kbd className="inline">marimo config show</Kbd> in your terminal to
show your current configuration and file location.
</p>
);
return (
<Form {...form}>
<form
ref={formElement}
onChange={form.handleSubmit(onSubmit)}
className="flex text-pretty overflow-hidden"
>
<Tabs
value={activeCategory}
onValueChange={(value) =>
setActiveCategory(value as SettingCategoryId)
}
orientation="vertical"
className="w-1/3 pr-4 border-r h-full overflow-auto p-6"
>
<TabsList className="self-start max-h-none flex flex-col gap-2 shrink-0 bg-background flex-1 min-h-full">
{categories.map((category) => (
<TabsTrigger
key={category.id}
value={category.id}
className="w-full text-left p-2 data-[state=active]:bg-primary data-[state=active]:text-primary-foreground justify-start"
>
<div className="flex gap-4 items-center text-lg">
<span
className={cn(
category.className,
"w-8 h-8 rounded flex items-center justify-center text-muted-foreground",
)}
>
<category.Icon className="w-4 h-4" />
</span>
{category.label}
</div>
</TabsTrigger>
))}
<div className="flex-1" />
{!isWasm() && configMessage}
</TabsList>
</Tabs>
<div className="w-2/3 pl-6 gap-2 flex flex-col overflow-auto p-6">
{renderBody()}
</div>
</form>
</Form>
);
};
const SettingGroup = ({
title,
children,
}: { title: string; children: React.ReactNode }) => {
return (
<div className="flex flex-col gap-4 pb-4">
<SettingSubtitle className="text-base">{title}</SettingSubtitle>
{children}
</div>
);
};
const IsOverridden = ({
userConfig,
name,
}: { userConfig: UserConfig; name: FieldPath<UserConfig> }) => {
const currentValue = get(userConfig, name);
const overrides = useAtomValue(configOverridesAtom);
const overriddenValue = get(overrides as UserConfig, name);
if (overriddenValue == null) {
return null;
}
if (currentValue === overriddenValue) {
return null;
}
return (
<Tooltip
content={
<>
<span>
This setting is overridden by{" "}
<Kbd className="inline">pyproject.toml</Kbd>.
</span>
<br />
<span>
Edit the <Kbd className="inline">pyproject.toml</Kbd> file directly
to change this setting.
</span>
<br />
<span>
User value: <strong>{String(currentValue)}</strong>
</span>
<br />
<span>
Project value: <strong>{String(overriddenValue)}</strong>
</span>
</>
}
>
<span className="text-[var(--amber-12)] text-xs flex items-center gap-1 border rounded px-2 py-1 bg-[var(--amber-2)] border-[var(--amber-6)] ml-1">
<FolderCog2 className="w-3 h-3" />
Overridden by pyproject.toml [{String(overriddenValue)}]
</span>
</Tooltip>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/chat/markdown-renderer.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { useTheme } from "@/theme/useTheme";
import { LazyAnyLanguageCodeMirror } from "@/plugins/impl/code/LazyAnyLanguageCodeMirror";
import { useCellActions } from "@/core/cells/cells";
import { Button } from "@/components/ui/button";
import { BetweenHorizontalStartIcon } from "lucide-react";
import { EditorView } from "@codemirror/view";
import Markdown, { type Components } from "react-markdown";
import { useEffect, useState } from "react";
import { useLastFocusedCellId } from "@/core/cells/focus";
import { copyToClipboard } from "@/utils/copy";
import { SQLLanguageAdapter } from "@/core/codemirror/language/sql";
import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
import { useAtomValue } from "jotai";
import { autoInstantiateAtom } from "@/core/config/config";
import { MarkdownLanguageAdapter } from "@/core/codemirror/language/markdown";
const extensions = [EditorView.lineWrapping];
interface CodeBlockProps {
code: string;
language: string | undefined;
}
const SUPPORTED_LANGUAGES = new Set([
"python",
"markdown",
"sql",
"json",
"yaml",
"toml",
"shell",
"javascript",
"typescript",
"jsx",
"tsx",
"css",
"html",
]);
function maybeTransform(
language: string | undefined,
code: string,
): {
language: string;
code: string;
} {
// Default to python
if (!language) {
return { language: "python", code };
}
// Already in the right language
if (language === "python") {
return { language, code };
}
// Convert to python
if (language === "sql") {
return { language: "python", code: SQLLanguageAdapter.fromQuery(code) };
}
// Convert to python
if (language === "markdown") {
return {
language: "python",
code: MarkdownLanguageAdapter.fromMarkdown(code),
};
}
// Run shell commands
if (language === "shell" || language === "bash") {
return {
language: "python",
code: `import subprocess\nsubprocess.run("${code}")`,
};
}
// Store as a string
return {
language: "python",
code: `_${language} = """\n${code}\n"""`,
};
}
const CodeBlock = ({ code, language }: CodeBlockProps) => {
const { theme } = useTheme();
const { createNewCell } = useCellActions();
const lastFocusedCellId = useLastFocusedCellId();
const autoInstantiate = useAtomValue(autoInstantiateAtom);
const [value, setValue] = useState(code);
useEffect(() => {
setValue(code);
}, [code]);
const handleInsertCode = () => {
const result = maybeTransform(language, value);
if (language === "sql") {
maybeAddMarimoImport(autoInstantiate, createNewCell, lastFocusedCellId);
}
createNewCell({
code: result.code,
before: false,
cellId: lastFocusedCellId ?? "__end__",
});
};
const handleCopyCode = async () => {
await copyToClipboard(value);
};
return (
<div className="relative">
<LazyAnyLanguageCodeMirror
theme={theme === "dark" ? "dark" : "light"}
// Only show the language if it's supported
language={
language && SUPPORTED_LANGUAGES.has(language) ? language : undefined
}
className="cm border rounded overflow-hidden"
extensions={extensions}
value={value}
onChange={setValue}
/>
<div className="flex justify-end mt-2 space-x-2">
<Button size="xs" variant="outline" onClick={handleCopyCode}>
Copy
</Button>
<Button size="xs" variant="outline" onClick={handleInsertCode}>
Add to Notebook
<BetweenHorizontalStartIcon className="ml-2 h-4 w-4" />
</Button>
</div>
</div>
);
};
const COMPONENTS: Components = {
code: ({ children, className }) => {
const language = className?.replace("language-", "");
if (language && typeof children === "string") {
return (
<div>
<div className="text-xs text-muted-foreground pl-1">{language}</div>
<CodeBlock code={children.trim()} language={language} />
</div>
);
}
return <code className={className}>{children}</code>;
},
};
export const MarkdownRenderer = ({ content }: { content: string }) => {
return (
<Markdown
components={COMPONENTS}
className="prose dark:prose-invert max-w-none prose-pre:pl-0"
>
{content}
</Markdown>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/chart-spec-model.test.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { describe, it, expect } from "vitest";
import { ColumnChartSpecModel } from "../chart-spec-model";
import type { ColumnHeaderSummary, FieldTypes } from "../types";
describe("ColumnChartSpecModel", () => {
const mockData = "http://example.com/data.json";
const mockFieldTypes: FieldTypes = {
date: "date",
number: "number",
integer: "integer",
boolean: "boolean",
string: "string",
};
const mockSummaries: ColumnHeaderSummary[] = [
{ column: "date", min: "2023-01-01", max: "2023-12-31" },
{ column: "number", min: 0, max: 100 },
{ column: "integer", min: 1, max: 10 },
{ column: "boolean", true: 5, false: 5 },
{ column: "string", unique: 20 },
];
it("should create an instance", () => {
const model = new ColumnChartSpecModel(
mockData,
mockFieldTypes,
mockSummaries,
{ includeCharts: true },
);
expect(model).toBeInstanceOf(ColumnChartSpecModel);
});
it("should return EMPTY for static EMPTY property", () => {
expect(ColumnChartSpecModel.EMPTY).toBeInstanceOf(ColumnChartSpecModel);
expect(ColumnChartSpecModel.EMPTY.summaries).toEqual([]);
});
it("should return header summary with spec when includeCharts is true", () => {
const model = new ColumnChartSpecModel(
mockData,
mockFieldTypes,
mockSummaries,
{ includeCharts: true },
);
const dateSummary = model.getHeaderSummary("date");
expect(dateSummary.summary).toEqual(mockSummaries[0]);
expect(dateSummary.type).toBe("date");
expect(dateSummary.spec).toBeDefined();
});
it("should return header summary without spec when includeCharts is false", () => {
const model = new ColumnChartSpecModel(
mockData,
mockFieldTypes,
mockSummaries,
{ includeCharts: false },
);
const numberSummary = model.getHeaderSummary("number");
expect(numberSummary.summary).toEqual(mockSummaries[1]);
expect(numberSummary.type).toBe("number");
expect(numberSummary.spec).toBeUndefined();
});
it("should return null spec for string and unknown types", () => {
const model = new ColumnChartSpecModel(
mockData,
mockFieldTypes,
mockSummaries,
{ includeCharts: true },
);
const stringSummary = model.getHeaderSummary("string");
expect(stringSummary.spec).toBeNull();
});
it("should handle special characters in column names", () => {
const specialFieldTypes: FieldTypes = {
"column.with[special:chars]": "number",
};
const specialSummaries: ColumnHeaderSummary[] = [
{ column: "column.with[special:chars]", min: 0, max: 100 },
];
const model = new ColumnChartSpecModel(
mockData,
specialFieldTypes,
specialSummaries,
{ includeCharts: true },
);
const summary = model.getHeaderSummary("column.with[special:chars]");
expect(summary.spec).toBeDefined();
expect((summary.spec?.encoding?.x as { field: string })?.field).toBe(
"column\\.with\\[special\\:chars\\]",
);
});
describe("snapshot", () => {
const fieldTypes: FieldTypes = {
...mockFieldTypes,
a: "number",
};
it("url data", () => {
const model = new ColumnChartSpecModel(
mockData,
fieldTypes,
mockSummaries,
{ includeCharts: true },
);
expect(model.getHeaderSummary("date").spec).toMatchSnapshot();
});
it("csv data", () => {
const model = new ColumnChartSpecModel(
`data:text/csv;base64,${btoa("a,b,c\n1,2,3\n4,5,6")}`,
fieldTypes,
mockSummaries,
{ includeCharts: true },
);
expect(model.getHeaderSummary("a").spec).toMatchSnapshot();
});
it("csv string", () => {
const model = new ColumnChartSpecModel(
"a,b,c\n1,2,3\n4,5,6",
fieldTypes,
mockSummaries,
{ includeCharts: true },
);
expect(model.getHeaderSummary("a").spec).toMatchSnapshot();
});
it("array", () => {
const model = new ColumnChartSpecModel(
["a", "b", "c"],
fieldTypes,
mockSummaries,
{ includeCharts: true },
);
expect(model.getHeaderSummary("a").spec).toMatchSnapshot();
});
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/column_formatting.test.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { describe, expect, it } from "vitest";
import { applyFormat } from "../column-formatting/feature";
import {
prettyScientificNumber,
prettyEngineeringNumber,
} from "@/utils/numbers";
describe("applyFormat", () => {
it("should return an empty string for null, undefined, or empty string values", () => {
expect(applyFormat(null, "Date", "date")).toBe("");
expect(applyFormat(undefined, "Date", "date")).toBe("");
expect(applyFormat("", "Date", "date")).toBe("");
});
describe("date formatting", () => {
it("should format date values correctly", () => {
const date = "2023-10-01T12:00:00Z";
expect(applyFormat(date, "Date", "date")).toBe("10/1/23");
expect(applyFormat(date, "Datetime", "date")).toBe(
"10/1/23, 12:00:00 PM UTC",
);
});
it("should format time values correctly", () => {
const time = "12:00:00Z";
expect(applyFormat(time, "Time", "time")).toBe("12:00:00Z");
});
it("should format datetime values correctly", () => {
const datetime = "2023-10-01T12:00:00Z";
expect(applyFormat(datetime, "Datetime", "datetime")).toBe(
"10/1/23, 12:00:00 PM UTC",
);
});
});
describe("number formatting", () => {
it("should format number values correctly", () => {
const number = "1234.567";
expect(applyFormat(number, "Auto", "number")).toBe("1,234.57");
expect(applyFormat(number, "Percent", "number")).toBe("123,456.7%");
expect(applyFormat(number, "Scientific", "number")).toBe(
prettyScientificNumber(1234.567),
);
expect(applyFormat(number, "Engineering", "number")).toBe(
prettyEngineeringNumber(1234.567),
);
expect(applyFormat(number, "Integer", "number")).toBe("1,235");
});
});
describe("string formatting", () => {
it("should format string values correctly", () => {
const str = "hello world";
expect(applyFormat(str, "Uppercase", "string")).toBe("HELLO WORLD");
expect(applyFormat(str, "Lowercase", "string")).toBe("hello world");
expect(applyFormat(str, "Capitalize", "string")).toBe("Hello world");
expect(applyFormat(str, "Title", "string")).toBe("Hello World");
});
});
describe("boolean formatting", () => {
it("should format boolean values correctly", () => {
expect(applyFormat(true, "Yes/No", "boolean")).toBe("Yes");
expect(applyFormat(false, "Yes/No", "boolean")).toBe("No");
expect(applyFormat(true, "On/Off", "boolean")).toBe("On");
expect(applyFormat(false, "On/Off", "boolean")).toBe("Off");
});
});
it("should return the original value for unknown data types or formats", () => {
expect(applyFormat("some value", "Auto", "unknown")).toBe("some value");
expect(applyFormat(123, "Auto", "unknown")).toBe(123);
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/columns.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { describe, expect, it, test } from "vitest";
import { uniformSample } from "../uniformSample";
import { UrlDetector } from "../url-detector";
import { render } from "@testing-library/react";
import { generateColumns, inferFieldTypes } from "../columns";
import type { FieldTypesWithExternalType } from "../types";
test("uniformSample", () => {
const items = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"];
expect(uniformSample(items, 2)).toMatchInlineSnapshot(`
[
"A",
"J",
]
`);
expect(uniformSample(items, 4)).toMatchInlineSnapshot(`
[
"A",
"C",
"F",
"J",
]
`);
expect(uniformSample(items, 100)).toBe(items);
});
test("UrlDetector renders URLs as hyperlinks", () => {
const text = "Check this link: https://example.com";
const { container } = render(<UrlDetector text={text} />);
const link = container.querySelector("a");
expect(link).toBeTruthy();
expect(link?.href).toBe("https://example.com/");
});
test("inferFieldTypes", () => {
const data = [
{
a: 1,
b: "foo",
c: null,
d: { mime: "text/csv" },
e: [1, 2, 3],
f: true,
g: false,
h: new Date(),
},
];
const fieldTypes = inferFieldTypes(data);
expect(fieldTypes).toMatchInlineSnapshot(`
[
[
"a",
[
"number",
"number",
],
],
[
"b",
[
"string",
"string",
],
],
[
"c",
[
"unknown",
"object",
],
],
[
"d",
[
"unknown",
"object",
],
],
[
"e",
[
"unknown",
"object",
],
],
[
"f",
[
"boolean",
"boolean",
],
],
[
"g",
[
"boolean",
"boolean",
],
],
[
"h",
[
"datetime",
"datetime",
],
],
]
`);
});
test("inferFieldTypes with nulls", () => {
const data = [{ a: 1, b: null }];
const fieldTypes = inferFieldTypes(data);
expect(fieldTypes).toMatchInlineSnapshot(`
[
[
"a",
[
"number",
"number",
],
],
[
"b",
[
"unknown",
"object",
],
],
]
`);
});
test("inferFieldTypes with mimetypes", () => {
const data = [{ a: { mime: "text/csv" }, b: { mime: "image/png" } }];
const fieldTypes = inferFieldTypes(data);
expect(fieldTypes).toMatchInlineSnapshot(`
[
[
"a",
[
"unknown",
"object",
],
],
[
"b",
[
"unknown",
"object",
],
],
]
`);
});
describe("generateColumns", () => {
const fieldTypes: FieldTypesWithExternalType = [
["name", ["string", "text"]],
["age", ["number", "integer"]],
];
it("should generate columns with row headers", () => {
const columns = generateColumns({
rowHeaders: ["name"],
selection: null,
fieldTypes,
});
expect(columns).toHaveLength(3);
expect(columns[0].id).toBe("name");
expect(columns[0].meta?.rowHeader).toBe(true);
expect(columns[0].enableSorting).toBe(true);
});
it("should generate columns with nameless row headers", () => {
const columns = generateColumns({
rowHeaders: [""],
selection: null,
fieldTypes,
});
expect(columns).toHaveLength(3);
expect(columns[0].id).toMatchInlineSnapshot(`"__m_column__0"`);
expect(columns[0].meta?.rowHeader).toBe(true);
expect(columns[0].enableSorting).toBe(false);
});
it("should include selection column for multi selection", () => {
const columns = generateColumns({
rowHeaders: [],
selection: "multi",
fieldTypes,
});
expect(columns[0].id).toBe("__select__");
expect(columns[0].enableSorting).toBe(false);
});
it("should generate columns with correct meta data", () => {
const columns = generateColumns({
rowHeaders: [],
selection: null,
fieldTypes,
});
expect(columns.length).toBe(2);
expect(columns[0].meta?.dataType).toBe("string");
expect(columns[1].meta?.dataType).toBe("number");
});
it("should handle text justification and wrapping", () => {
const columns = generateColumns({
rowHeaders: [],
selection: null,
fieldTypes,
textJustifyColumns: { name: "center" },
wrappedColumns: ["age"],
});
// Assuming getCellStyleClass is a function that returns a class name
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const cell = (columns[0].cell as any)({
column: columns[0],
renderValue: () => "John",
getValue: () => "John",
});
expect(cell?.props.className).toContain("center");
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/pagination.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { expect, test } from "vitest";
import { render } from "@testing-library/react";
import { PageSelector } from "../pagination";
import { Functions } from "@/utils/functions";
function getOptions(currentPage: number) {
const { container } = render(
<PageSelector
currentPage={currentPage}
totalPages={200}
onPageChange={Functions.NOOP}
/>,
);
const options = container.querySelectorAll("option");
const optionValues = [...options].map((option) => option.textContent);
return optionValues;
}
test("pagination start / middle / end", () => {
expect(getOptions(1)).toMatchInlineSnapshot(`
[
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"...",
"96",
"97",
"98",
"99",
"100",
"101",
"102",
"103",
"104",
"105",
"...",
"191",
"192",
"193",
"194",
"195",
"196",
"197",
"198",
"199",
"200",
]
`);
// all fall in the top/middle/bottom 10
expect(getOptions(1)).toEqual(getOptions(10));
expect(getOptions(96)).toEqual(getOptions(105));
expect(getOptions(191)).toEqual(getOptions(200));
// Check off by one
expect(getOptions(1)).not.toEqual(getOptions(11));
expect(getOptions(1)).not.toEqual(getOptions(95));
expect(getOptions(1)).not.toEqual(getOptions(106));
expect(getOptions(1)).not.toEqual(getOptions(190));
});
test("pagination lower middle", () => {
expect(getOptions(50)).toMatchInlineSnapshot(`
[
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"...",
"50",
"...",
"96",
"97",
"98",
"99",
"100",
"101",
"102",
"103",
"104",
"105",
"...",
"191",
"192",
"193",
"194",
"195",
"196",
"197",
"198",
"199",
"200",
]
`);
});
test("pagination upper middle", () => {
expect(getOptions(150)).toMatchInlineSnapshot(`
[
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"...",
"96",
"97",
"98",
"99",
"100",
"101",
"102",
"103",
"104",
"105",
"...",
"150",
"...",
"191",
"192",
"193",
"194",
"195",
"196",
"197",
"198",
"199",
"200",
]
`);
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/__snapshots__/chart-spec-model.test.ts.snap
```snap
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`ColumnChartSpecModel > snapshot > array 1`] = `
{
"background": "transparent",
"config": {
"axis": {
"domain": false,
},
"view": {
"stroke": "transparent",
},
},
"data": {
"values": [
"a",
"b",
"c",
],
},
"encoding": {
"color": {
"condition": {
"test": "datum["bin_maxbins_10_a_range"] === "null"",
"value": "#cc4e00",
},
"value": "#027864",
},
"tooltip": [
{
"bin": true,
"field": "a",
"format": ".2f",
"title": "a",
"type": "nominal",
},
{
"aggregate": "count",
"format": ",d",
"title": "Count",
"type": "quantitative",
},
],
"x": {
"axis": null,
"bin": true,
"field": "a",
"scale": {
"align": 0,
"paddingInner": 0,
"paddingOuter": {
"expr": "length(data('source_0')) == 2 ? 1 : length(data('source_0')) == 3 ? 0.5 : length(data('source_0')) == 4 ? 0 : 0",
},
},
"type": "nominal",
},
"y": {
"aggregate": "count",
"axis": null,
"scale": {
"type": "linear",
},
"type": "quantitative",
},
},
"height": 100,
"mark": {
"align": "right",
"color": "#027864",
"size": {
"expr": "min(28, 120 / length(data('source_0')) - 1)",
},
"type": "bar",
},
}
`;
exports[`ColumnChartSpecModel > snapshot > csv data 1`] = `
{
"background": "transparent",
"config": {
"axis": {
"domain": false,
},
"view": {
"stroke": "transparent",
},
},
"data": {
"values": [
{
"a": 1,
"b": 2,
"c": 3,
},
{
"a": 4,
"b": 5,
"c": 6,
},
],
},
"encoding": {
"color": {
"condition": {
"test": "datum["bin_maxbins_10_a_range"] === "null"",
"value": "#cc4e00",
},
"value": "#027864",
},
"tooltip": [
{
"bin": true,
"field": "a",
"format": ".2f",
"title": "a",
"type": "nominal",
},
{
"aggregate": "count",
"format": ",d",
"title": "Count",
"type": "quantitative",
},
],
"x": {
"axis": null,
"bin": true,
"field": "a",
"scale": {
"align": 0,
"paddingInner": 0,
"paddingOuter": {
"expr": "length(data('data_0')) == 2 ? 1 : length(data('data_0')) == 3 ? 0.5 : length(data('data_0')) == 4 ? 0 : 0",
},
},
"type": "nominal",
},
"y": {
"aggregate": "count",
"axis": null,
"scale": {
"type": "linear",
},
"type": "quantitative",
},
},
"height": 100,
"mark": {
"align": "right",
"color": "#027864",
"size": {
"expr": "min(28, 120 / length(data('data_0')) - 1)",
},
"type": "bar",
},
}
`;
exports[`ColumnChartSpecModel > snapshot > csv string 1`] = `
{
"background": "transparent",
"config": {
"axis": {
"domain": false,
},
"view": {
"stroke": "transparent",
},
},
"data": {
"values": [
{
"a": 1,
"b": 2,
"c": 3,
},
{
"a": 4,
"b": 5,
"c": 6,
},
],
},
"encoding": {
"color": {
"condition": {
"test": "datum["bin_maxbins_10_a_range"] === "null"",
"value": "#cc4e00",
},
"value": "#027864",
},
"tooltip": [
{
"bin": true,
"field": "a",
"format": ".2f",
"title": "a",
"type": "nominal",
},
{
"aggregate": "count",
"format": ",d",
"title": "Count",
"type": "quantitative",
},
],
"x": {
"axis": null,
"bin": true,
"field": "a",
"scale": {
"align": 0,
"paddingInner": 0,
"paddingOuter": {
"expr": "length(data('data_0')) == 2 ? 1 : length(data('data_0')) == 3 ? 0.5 : length(data('data_0')) == 4 ? 0 : 0",
},
},
"type": "nominal",
},
"y": {
"aggregate": "count",
"axis": null,
"scale": {
"type": "linear",
},
"type": "quantitative",
},
},
"height": 100,
"mark": {
"align": "right",
"color": "#027864",
"size": {
"expr": "min(28, 120 / length(data('data_0')) - 1)",
},
"type": "bar",
},
}
`;
exports[`ColumnChartSpecModel > snapshot > url data 1`] = `
{
"background": "transparent",
"config": {
"axis": {
"domain": false,
},
"view": {
"stroke": "transparent",
},
},
"data": {
"values": [],
},
"encoding": {
"color": {
"condition": {
"test": "datum["bin_maxbins_10_date_range"] === "null"",
"value": "#cc4e00",
},
"value": "#027864",
},
"tooltip": [
{
"bin": true,
"field": "date",
"format": "%Y-%m-%d",
"title": "date",
"type": "temporal",
},
{
"aggregate": "count",
"format": ",d",
"title": "Count",
"type": "quantitative",
},
],
"x": {
"axis": null,
"bin": true,
"field": "date",
"scale": {
"align": 0,
"paddingInner": 0,
"paddingOuter": {
"expr": "length(data('data_0')) == 2 ? 1 : length(data('data_0')) == 3 ? 0.5 : length(data('data_0')) == 4 ? 0 : 0",
},
},
"type": "temporal",
},
"y": {
"aggregate": "count",
"axis": null,
"type": "quantitative",
},
},
"height": 100,
"mark": {
"color": "#027864",
"type": "bar",
"width": {
"expr": "min(28, 120 / length(data('data_0')) - 1)",
},
},
}
`;
```
File: /Users/morganmcguire/ML/marimo/frontend/public/circle-play.ico
```ico
00®(0` —ã≈Ñ«É«Ñ«Ñ∆Ñ»Ö∆Ö∆Ñ∆Ç∆֫ѫѻ֫ѫÜ∆Ö«Ö∆É«Ñ«É«Ñ«É«Ñˇˇˇ  





 

 



  

  







  

  



 

 





  ˇˇˇˇˇˇˇˇˇˇˇˇˇˇÄˇˇˇ˛ˇˇ¯ˇˇ‡ˇˇ¿ˇˇÄˇˇˇ˛¸?¯¯‡‡¿¿¿¿¿¿¿¿¿¿¿¿¿¿‡‡¯¯¸?˛ˇˇˇÄˇˇ¿ˇˇ‡ˇˇ¯ˇˇ˛ˇˇˇÄˇˇˇˇˇˇˇˇˇˇˇˇˇˇ
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__test__/useColumnPinning.test.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { SELECT_COLUMN_ID } from "../types";
import { renderHook, act } from "@testing-library/react-hooks";
import { describe, it, expect } from "vitest";
import { useColumnPinning } from "../hooks/useColumnPinning";
describe("useColumnPinning", () => {
it("should initialize with correct default values", () => {
const { result } = renderHook(() => useColumnPinning());
expect(result.current.columnPinning).toEqual({
left: [],
right: undefined,
});
});
it("should add SELECT_COLUMN_ID to left when freezeColumnsLeft is provided", () => {
const { result } = renderHook(() =>
useColumnPinning(["column1", "column2"]),
);
expect(result.current.columnPinning.left).toEqual([
SELECT_COLUMN_ID,
"column1",
"column2",
]);
});
it("should not add SELECT_COLUMN_ID if it's already present", () => {
const { result } = renderHook(() =>
useColumnPinning([SELECT_COLUMN_ID, "column1"]),
);
expect(result.current.columnPinning.left).toEqual([
SELECT_COLUMN_ID,
"column1",
]);
});
it("should set right columns correctly", () => {
const { result } = renderHook(() =>
useColumnPinning(undefined, ["column3", "column4"]),
);
expect(result.current.columnPinning.right).toEqual(["column3", "column4"]);
});
it("should update column pinning state correctly", () => {
const { result } = renderHook(() => useColumnPinning());
act(() => {
result.current.setColumnPinning({
left: ["column1"],
right: ["column2"],
});
});
expect(result.current.columnPinning).toEqual({
left: [SELECT_COLUMN_ID, "column1"],
right: ["column2"],
});
});
it("should handle function updates to column pinning state", () => {
const { result } = renderHook(() => useColumnPinning(["initialLeft"]));
act(() => {
result.current.setColumnPinning((prev) => ({
...prev,
right: ["newRight"],
}));
});
expect(result.current.columnPinning).toEqual({
left: [SELECT_COLUMN_ID, "initialLeft"],
right: ["newRight"],
});
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__tests__/data-table.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { render } from "@testing-library/react";
import { DataTable } from "../data-table";
import { describe, expect, it } from "vitest";
import { vi } from "vitest";
import type { ColumnDef } from "@tanstack/react-table";
import type { RowSelectionState } from "@tanstack/react-table";
interface TestData {
id: number;
name: string;
}
describe("DataTable", () => {
it("should maintain selection state when remounted", () => {
const mockOnRowSelectionChange = vi.fn();
const testData: TestData[] = [
{ id: 1, name: "Test 1" },
{ id: 2, name: "Test 2" },
];
const columns: Array<ColumnDef<TestData>> = [
{ accessorKey: "name", header: "Name" },
];
const initialRowSelection: RowSelectionState = { "0": true };
const commonProps = {
data: testData,
columns,
selection: "single" as const,
totalRows: 2,
totalColumns: 1,
pagination: false,
rowSelection: initialRowSelection,
onRowSelectionChange: mockOnRowSelectionChange,
};
const { rerender } = render(<DataTable {...commonProps} />);
// Verify initial selection is not cleared
expect(mockOnRowSelectionChange).not.toHaveBeenCalledWith({});
// Simulate remount (as would happen in accordion toggle)
rerender(<DataTable {...commonProps} />);
// Verify selection is still not cleared after remount
expect(mockOnRowSelectionChange).not.toHaveBeenCalledWith({});
// Verify the rowSelection prop is maintained
expect(commonProps.rowSelection).toEqual(initialRowSelection);
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-formatting/feature.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import {
type TableFeature,
type RowData,
makeStateUpdater,
type Table,
type Column,
type Updater,
} from "@tanstack/react-table";
import type {
ColumnFormattingTableState,
ColumnFormattingOptions,
ColumnFormattingState,
} from "./types";
import type { DataType } from "@/core/kernel/messages";
import type { FormatOption } from "./types";
import {
prettyNumber,
prettyScientificNumber,
prettyEngineeringNumber,
} from "@/utils/numbers";
import { logNever } from "@/utils/assertNever";
export const ColumnFormattingFeature: TableFeature = {
// define the column formatting's initial state
getInitialState: (state): ColumnFormattingTableState => {
return {
columnFormatting: {},
...state,
};
},
// define the new column formatting's default options
getDefaultOptions: <TData extends RowData>(
table: Table<TData>,
): ColumnFormattingOptions => {
return {
enableColumnFormatting: true,
onColumnFormattingChange: makeStateUpdater("columnFormatting", table),
} as ColumnFormattingOptions;
},
createColumn: <TData extends RowData>(
column: Column<TData>,
table: Table<TData>,
) => {
column.getColumnFormatting = () => {
return table.getState().columnFormatting[column.id];
};
column.getCanFormat = () => {
return (
(table.options.enableColumnFormatting &&
column.columnDef.meta?.dataType !== "unknown" &&
column.columnDef.meta?.dataType !== undefined) ??
false
);
};
column.setColumnFormatting = (value) => {
const safeUpdater: Updater<ColumnFormattingState> = (old) => {
return {
...old,
[column.id]: value,
};
};
table.options.onColumnFormattingChange?.(safeUpdater);
};
// apply column formatting
column.applyColumnFormatting = (value) => {
const dataType = column.columnDef.meta?.dataType;
const format = column.getColumnFormatting?.();
if (format) {
return applyFormat(value, format, dataType);
}
return value;
};
},
};
const percentFormatter = new Intl.NumberFormat(undefined, {
style: "percent",
minimumFractionDigits: 0,
maximumFractionDigits: 2,
});
const dateFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "short", // 3/4/2024
});
const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
dateStyle: "short", // 3/4/2024
timeStyle: "long", // 3:04:05 PM
timeZone: "UTC",
});
const timeFormatter = new Intl.DateTimeFormat(undefined, {
timeStyle: "long", // 3:04:05 PM
timeZone: "UTC",
});
const integerFormatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 0, // 1,000,000
});
// Apply formatting to a value given a format and data type
export const applyFormat = (
value: unknown,
format: FormatOption,
dataType: DataType | undefined,
) => {
// If the value is null, return an empty string
if (value === null || value === undefined || value === "") {
return "";
}
// Handle date, number, string and boolean formatting
switch (dataType) {
case "time":
// Do nothing
return value;
case "datetime":
case "date": {
const date = new Date(value as string);
switch (format) {
case "Date":
return dateFormatter.format(date);
case "Datetime":
return dateTimeFormatter.format(date);
case "Time":
return timeFormatter.format(date);
default:
return value;
}
}
case "integer":
case "number": {
const num = Number.parseFloat(value as string);
switch (format) {
case "Auto":
return prettyNumber(num);
case "Percent":
return percentFormatter.format(num);
case "Scientific":
return prettyScientificNumber(num);
case "Engineering":
return prettyEngineeringNumber(num);
case "Integer":
return integerFormatter.format(num);
default:
return value;
}
}
case "string":
switch (format) {
case "Uppercase":
return (value as string).toUpperCase();
case "Lowercase":
return (value as string).toLowerCase();
case "Capitalize":
return (
(value as string).charAt(0).toUpperCase() +
(value as string).slice(1)
);
case "Title":
return (value as string)
.split(" ")
.map(
(word) =>
word.charAt(0).toUpperCase() + word.slice(1).toLowerCase(),
)
.join(" ");
default:
return value;
}
case "boolean":
switch (format) {
case "Yes/No":
return (value as boolean) ? "Yes" : "No";
case "On/Off":
return (value as boolean) ? "On" : "Off";
default:
return value;
}
case undefined:
case "unknown":
return value;
default:
logNever(dataType);
return value;
}
};
export function formattingExample(
format: FormatOption,
): string | number | undefined | null {
switch (format) {
case "Date":
return String(applyFormat(new Date(), "Date", "date"));
case "Datetime":
return String(applyFormat(new Date(), "Datetime", "date"));
case "Time":
return String(applyFormat(new Date(), "Time", "date"));
case "Percent":
return String(applyFormat(0.1234, "Percent", "number"));
case "Scientific":
return String(applyFormat(12_345_678_910, "Scientific", "number"));
case "Engineering":
return String(applyFormat(12_345_678_910, "Engineering", "number"));
case "Integer":
return String(applyFormat(1234.567, "Integer", "number"));
case "Auto":
return String(applyFormat(1234.567, "Auto", "number"));
default:
return null;
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-formatting/types.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
/* eslint-disable @typescript-eslint/no-empty-interface */
import type { DataType } from "@/core/kernel/messages";
import type { OnChangeFn, RowData } from "@tanstack/react-table";
// define all format options
export const formatOptions = {
date: ["Date", "Datetime", "Time"],
datetime: ["Date", "Datetime", "Time"],
time: [],
integer: ["Auto", "Percent", "Scientific", "Engineering", "Integer"],
number: ["Auto", "Percent", "Scientific", "Engineering", "Integer"],
string: ["Uppercase", "Lowercase", "Capitalize", "Title"],
boolean: ["Yes/No", "On/Off"],
unknown: [],
} as const satisfies Record<DataType, string[]>;
// define types for format options
export type FormatOptions = (typeof formatOptions)[keyof typeof formatOptions];
export type FormatOption = FormatOptions[number];
// define types for column formatting's custom state
export type ColumnFormattingState = Record<string, FormatOption | undefined>;
export interface ColumnFormattingTableState {
columnFormatting: ColumnFormattingState;
}
// define types for column formatting's table options
export interface ColumnFormattingOptions {
enableColumnFormatting?: boolean;
onColumnFormattingChange?: OnChangeFn<ColumnFormattingState>;
}
// define types for column formatting's table APIs
export interface ColumnFormattingInstance {
setColumnFormatting: (value?: FormatOption) => void;
getColumnFormatting?: () => FormatOption | undefined;
getCanFormat?: () => boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
applyColumnFormatting: (value: any) => any;
}
// Use declaration merging to add APIs and state types
// to TanStack Table's existing types.
declare module "@tanstack/react-table" {
//merge column formatting's state with the existing table state
interface TableState extends ColumnFormattingTableState {}
//merge column formatting's options with the existing table options
interface TableOptionsResolved<TData extends RowData>
extends ColumnFormattingOptions {}
//merge column formatting's instance APIs with the existing table instance APIs
interface Column<TData extends RowData> extends ColumnFormattingInstance {}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-wrapping/types.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
/* eslint-disable @typescript-eslint/no-empty-interface */
import type { OnChangeFn, RowData } from "@tanstack/react-table";
export type ColumnWrappingState = Record<string, "nowrap" | "wrap" | undefined>;
export interface ColumnWrappingTableState {
columnWrapping: ColumnWrappingState;
}
export interface ColumnWrappingOptions {
enableColumnWrapping?: boolean;
onColumnWrappingChange?: OnChangeFn<ColumnWrappingState>;
}
export interface ColumnWrappingInstance {
toggleColumnWrapping: (value?: "nowrap" | "wrap") => void;
getColumnWrapping?: () => "nowrap" | "wrap";
getCanWrap?: () => boolean;
}
// Use declaration merging to add our new feature APIs
declare module "@tanstack/react-table" {
interface TableState extends ColumnWrappingTableState {}
interface TableOptionsResolved<TData extends RowData>
extends ColumnWrappingOptions {}
interface Column<TData extends RowData> extends ColumnWrappingInstance {}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-wrapping/feature.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import {
type TableFeature,
type RowData,
makeStateUpdater,
type Table,
type Column,
type Updater,
} from "@tanstack/react-table";
import type {
ColumnWrappingTableState,
ColumnWrappingOptions,
ColumnWrappingState,
} from "./types";
export const ColumnWrappingFeature: TableFeature = {
getInitialState: (state): ColumnWrappingTableState => {
return {
columnWrapping: {},
...state,
};
},
getDefaultOptions: <TData extends RowData>(
table: Table<TData>,
): ColumnWrappingOptions => {
return {
enableColumnWrapping: true,
onColumnWrappingChange: makeStateUpdater("columnWrapping", table),
} as ColumnWrappingOptions;
},
createColumn: <TData extends RowData>(
column: Column<TData>,
table: Table<TData>,
) => {
column.getColumnWrapping = () => {
return table.getState().columnWrapping[column.id] || "nowrap";
};
column.getCanWrap = () => {
return table.options.enableColumnWrapping ?? false;
};
column.toggleColumnWrapping = (value?: "nowrap" | "wrap") => {
const safeUpdater: Updater<ColumnWrappingState> = (old) => {
const prevValue = old[column.id] || "nowrap";
if (value) {
return {
...old,
[column.id]: value,
};
}
return {
...old,
[column.id]: prevValue === "nowrap" ? "wrap" : "nowrap",
};
};
table.options.onColumnWrappingChange?.(safeUpdater);
};
},
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/__tests__/url-detector.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { render, screen, fireEvent } from "@testing-library/react";
import { describe, it, expect, vi } from "vitest";
import { UrlDetector } from "../url-detector";
describe("UrlDetector", () => {
it("renders plain text without URLs", () => {
render(<UrlDetector text="Hello world" />);
expect(screen.getByText("Hello world")).toBeInTheDocument();
});
it("renders regular URLs as clickable links", () => {
render(<UrlDetector text="Check https://marimo.io for more" />);
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "https://marimo.io");
expect(link).toHaveAttribute("target", "_blank");
expect(link).toHaveAttribute("rel", "noopener noreferrer");
});
it("renders multiple URLs in text", () => {
render(
<UrlDetector text="Visit https://marimo.io and https://github.com/marimo-team" />,
);
const links = screen.getAllByRole("link");
expect(links).toHaveLength(2);
expect(links[0]).toHaveAttribute("href", "https://marimo.io");
expect(links[1]).toHaveAttribute("href", "https://github.com/marimo-team");
});
it("renders image URLs as images", () => {
render(<UrlDetector text="Image: https://example.com/image.png" />);
const img = screen.getByRole("img");
expect(img).toHaveAttribute("src", "https://example.com/image.png");
expect(img).toHaveAttribute("alt", "URL preview");
});
it("renders known image domains as images", () => {
render(
<UrlDetector text="Avatar: https://avatars.githubusercontent.com/u/123" />,
);
const img = screen.getByRole("img");
expect(img).toHaveAttribute(
"src",
"https://avatars.githubusercontent.com/u/123",
);
});
it("falls back to link when image fails to load", () => {
render(<UrlDetector text="Broken image: https://example.com/broken.png" />);
const img = screen.getByRole("img");
// Simulate image load error
fireEvent.error(img);
// Should now be a link
const link = screen.getByRole("link");
expect(link).toHaveAttribute("href", "https://example.com/broken.png");
expect(img).not.toBeInTheDocument();
});
it("handles various image extensions", () => {
const extensions = ["png", "jpg", "jpeg", "gif", "webp", "svg", "ico"];
extensions.forEach((ext) => {
const { container } = render(
<UrlDetector text={`Image: https://example.com/image.${ext}`} />,
);
const img = container.querySelector("img");
expect(img).toHaveAttribute("src", `https://example.com/image.${ext}`);
});
});
it("renders data URIs as images", () => {
const dataUri =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
render(<UrlDetector text={dataUri} />);
const img = screen.getByRole("img");
expect(img).toHaveAttribute("src", dataUri);
});
it.skip("prevents event propagation on link clicks", () => {
const mockStopPropagation = vi.fn();
render(<UrlDetector text="Check https://marimo.io" />);
const link = screen.getByRole("link");
fireEvent.click(link, {
stopPropagation: mockStopPropagation,
});
expect(mockStopPropagation).toHaveBeenCalled();
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/chart-spec-model.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import type { TopLevelFacetedUnitSpec } from "@/plugins/impl/data-explorer/queries/types";
import { mint, orange, slate } from "@radix-ui/colors";
import type { ColumnHeaderSummary, FieldTypes } from "./types";
import { asURL } from "@/utils/url";
import { parseCsvData } from "@/plugins/impl/vega/loader";
import { logNever } from "@/utils/assertNever";
import type { TopLevelSpec } from "vega-lite";
const MAX_BAR_HEIGHT = 24; // px
const MAX_BAR_WIDTH = 28; // px
const CONTAINER_WIDTH = 120; // px
const PAD = 1; // px
export class ColumnChartSpecModel<T> {
private columnSummaries = new Map<string | number, ColumnHeaderSummary>();
public static readonly EMPTY = new ColumnChartSpecModel([], {}, [], {
includeCharts: false,
});
private dataSpec: TopLevelSpec["data"];
private sourceName: "data_0" | "source_0";
constructor(
private readonly data: T[] | string,
private readonly fieldTypes: FieldTypes,
readonly summaries: ColumnHeaderSummary[],
private readonly opts: {
includeCharts: boolean;
},
) {
// Data may come in from a few different sources:
// - A URL
// - A CSV data URI (e.g. "data:text/csv;base64,...")
// - A CSV string (e.g. "a,b,c\n1,2,3\n4,5,6")
// - An array of objects
// For each case, we need to set up the data spec and source name appropriately.
// If its a file, the source name will be "source_0", otherwise it will be "data_0".
// We have a few snapshot tests to ensure that the spec is correct for each case.
if (typeof this.data === "string") {
if (this.data.startsWith("./@file") || this.data.startsWith("/@file")) {
this.dataSpec = {
url: asURL(this.data).href,
};
this.sourceName = "source_0";
} else if (this.data.startsWith("data:text/csv;base64,")) {
const decoded = atob(this.data.split(",")[1]);
this.dataSpec = {
values: parseCsvData(decoded) as T[],
};
this.sourceName = "data_0";
} else {
// Assume it's a CSV string
this.dataSpec = {
values: parseCsvData(this.data) as T[],
};
this.sourceName = "data_0";
}
} else {
this.dataSpec = {
values: this.data,
};
this.sourceName = "source_0";
}
this.columnSummaries = new Map(summaries.map((s) => [s.column, s]));
}
public getHeaderSummary(column: string) {
return {
summary: this.columnSummaries.get(column),
type: this.fieldTypes[column],
spec: this.opts.includeCharts ? this.getVegaSpec(column) : undefined,
};
}
private getVegaSpec<T>(column: string): TopLevelFacetedUnitSpec | null {
if (!this.data) {
return null;
}
const base: Omit<TopLevelFacetedUnitSpec, "mark"> = {
data: this.dataSpec as TopLevelFacetedUnitSpec["data"],
background: "transparent",
config: {
view: {
stroke: "transparent",
},
axis: {
domain: false,
},
},
height: 100,
};
const type = this.fieldTypes[column];
// https://github.com/vega/altair/blob/32990a597af7c09586904f40b3f5e6787f752fa5/doc/user_guide/encodings/index.rst#escaping-special-characters-in-column-names
// escape periods in column names
column = column.replaceAll(".", "\\.");
// escape brackets in column names
column = column.replaceAll("[", "\\[").replaceAll("]", "\\]");
// escape colons in column names
column = column.replaceAll(":", "\\:");
const scale = this.getScale();
const variableWidth = `min(${MAX_BAR_WIDTH}, ${CONTAINER_WIDTH} / length(data('${this.sourceName}')) - ${PAD})`;
switch (type) {
case "date":
case "datetime":
case "time":
return {
...base,
mark: {
type: "bar",
color: mint.mint11,
width: { expr: variableWidth },
},
encoding: {
x: {
field: column,
type: "temporal",
axis: null,
bin: true,
scale: scale,
},
y: { aggregate: "count", type: "quantitative", axis: null },
tooltip: [
{
field: column,
type: "temporal",
format:
type === "date"
? "%Y-%m-%d"
: type === "time"
? "%H:%M:%S"
: "%Y-%m-%dT%H:%M:%S",
bin: true,
title: column,
},
{
aggregate: "count",
type: "quantitative",
title: "Count",
format: ",d",
},
],
// Color nulls
color: {
condition: {
test: `datum["bin_maxbins_10_${column}_range"] === "null"`,
value: orange.orange11,
},
value: mint.mint11,
},
},
};
case "integer":
case "number": {
const format = type === "integer" ? ",d" : ".2f";
return {
...base,
mark: {
type: "bar",
color: mint.mint11,
size: { expr: variableWidth },
align: "right",
},
encoding: {
x: {
field: column,
type: "nominal",
axis: null,
bin: true,
scale: scale,
},
y: {
aggregate: "count",
type: "quantitative",
axis: null,
scale: { type: "linear" },
},
tooltip: [
{
field: column,
type: "nominal",
format: format,
bin: true,
title: column,
},
{
aggregate: "count",
type: "quantitative",
title: "Count",
format: ",d",
},
],
// Color nulls
color: {
condition: {
test: `datum["bin_maxbins_10_${column}_range"] === "null"`,
value: orange.orange11,
},
value: mint.mint11,
},
},
};
}
case "boolean":
return {
...base,
mark: { type: "bar", color: mint.mint11 },
encoding: {
y: {
field: column,
type: "nominal",
axis: {
labelExpr:
"datum.label === 'true' || datum.label === 'True' ? 'True' : 'False'",
tickWidth: 0,
title: null,
labelColor: slate.slate9,
},
},
x: {
aggregate: "count",
type: "quantitative",
axis: null,
scale: { type: "linear" },
},
tooltip: [
{ field: column, type: "nominal", title: "Value" },
{
aggregate: "count",
type: "quantitative",
title: "Count",
format: ",d",
},
],
},
layer: [
{
mark: {
type: "bar",
color: mint.mint11,
height: MAX_BAR_HEIGHT,
},
},
{
mark: {
type: "text",
align: "left",
baseline: "middle",
dx: 3,
color: slate.slate9,
},
encoding: {
text: {
aggregate: "count",
type: "quantitative",
},
},
},
],
} as TopLevelFacetedUnitSpec; // "layer" not in TopLevelFacetedUnitSpec
case "unknown":
case "string":
return null;
default:
logNever(type);
return null;
}
}
private getScale() {
return {
align: 0,
paddingInner: 0,
paddingOuter: {
expr: `length(data('${this.sourceName}')) == 2 ? 1 : length(data('${this.sourceName}')) == 3 ? 0.5 : length(data('${this.sourceName}')) == 4 ? 0 : 0`,
},
};
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/hooks/useColumnPinning.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import React from "react";
import { SELECT_COLUMN_ID } from "../types";
import type { ColumnPinningState } from "@tanstack/react-table";
import { useInternalStateWithSync } from "@/hooks/useInternalStateWithSync";
import { isEqual } from "lodash-es";
interface UseColumnPinningResult {
columnPinning: ColumnPinningState;
setColumnPinning: React.Dispatch<React.SetStateAction<ColumnPinningState>>;
}
export function useColumnPinning(
freezeColumnsLeft?: string[],
freezeColumnsRight?: string[],
): UseColumnPinningResult {
const [columnPinning, setColumnPinning] =
useInternalStateWithSync<ColumnPinningState>(
{
left: maybeAddSelectColumnId(freezeColumnsLeft),
right: freezeColumnsRight,
},
isEqual,
);
const setColumnPinningWithFreeze = (
newState: React.SetStateAction<ColumnPinningState>,
) => {
setColumnPinning((prevState) => {
const updatedState =
typeof newState === "function" ? newState(prevState) : newState;
return {
left: maybeAddSelectColumnId(updatedState.left),
right: updatedState.right,
};
});
};
return { columnPinning, setColumnPinning: setColumnPinningWithFreeze };
}
function maybeAddSelectColumnId(freezeColumns: string[] | undefined): string[] {
if (!freezeColumns || freezeColumns.length === 0) {
return [];
}
return freezeColumns.includes(SELECT_COLUMN_ID)
? freezeColumns
: [SELECT_COLUMN_ID, ...freezeColumns];
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-header.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import type { Column } from "@tanstack/react-table";
import {
ChevronsUpDown,
ArrowDownNarrowWideIcon,
ArrowDownWideNarrowIcon,
CopyIcon,
FilterIcon,
FilterX,
MinusIcon,
SearchIcon,
WrapTextIcon,
AlignJustifyIcon,
PinOffIcon,
} from "lucide-react";
import { cn } from "@/utils/cn";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { Button } from "../ui/button";
import { useRef, useState } from "react";
import { NumberField } from "../ui/number-field";
import { Input } from "../ui/input";
import { type ColumnFilterForType, Filter } from "./filters";
import { logNever } from "@/utils/assertNever";
import type { DataType } from "@/core/kernel/messages";
import { formatOptions } from "./column-formatting/types";
import { DATA_TYPE_ICON } from "../datasets/icons";
import { formattingExample } from "./column-formatting/feature";
import { PinLeftIcon, PinRightIcon } from "@radix-ui/react-icons";
import { NAMELESS_COLUMN_PREFIX } from "./columns";
import { copyToClipboard } from "@/utils/copy";
interface DataTableColumnHeaderProps<TData, TValue>
extends React.HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
header: React.ReactNode;
}
export const DataTableColumnHeader = <TData, TValue>({
column,
header,
className,
}: DataTableColumnHeaderProps<TData, TValue>) => {
if (!header) {
return null;
}
if (!column.getCanSort() && !column.getCanFilter()) {
return <div className={cn(className)}>{header}</div>;
}
const AscIcon = ArrowDownNarrowWideIcon;
const DescIcon = ArrowDownWideNarrowIcon;
const renderSorts = () => {
if (!column.getCanSort()) {
return null;
}
return (
<>
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
<AscIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Asc
</DropdownMenuItem>
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
<DescIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Desc
</DropdownMenuItem>
{column.getIsSorted() && (
<DropdownMenuItem onClick={() => column.clearSorting()}>
<ChevronsUpDown className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Clear sort
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
</>
);
};
const renderColumnWrapping = () => {
if (!column.getCanWrap?.() || !column.getColumnWrapping) {
return null;
}
const wrap = column.getColumnWrapping();
if (wrap === "wrap") {
return (
<DropdownMenuItem
onClick={() => column.toggleColumnWrapping("nowrap")}
className="flex items-center"
>
<AlignJustifyIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
No wrap text
</DropdownMenuItem>
);
}
return (
<DropdownMenuItem
onClick={() => column.toggleColumnWrapping("wrap")}
className="flex items-center"
>
<WrapTextIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Wrap text
</DropdownMenuItem>
);
};
const renderColumnPinning = () => {
if (!column.getCanPin?.() || !column.getIsPinned) {
return null;
}
const pinnedPosition = column.getIsPinned();
if (pinnedPosition !== false) {
return (
<DropdownMenuItem
onClick={() => column.pin(false)}
className="flex items-center"
>
<PinOffIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Unfreeze
</DropdownMenuItem>
);
}
return (
<>
<DropdownMenuItem
onClick={() => column.pin("left")}
className="flex items-center"
>
<PinLeftIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Freeze left
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column.pin("right")}
className="flex items-center"
>
<PinRightIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Freeze right
</DropdownMenuItem>
</>
);
};
const dtype: string | undefined = column.columnDef.meta?.dtype;
const dataType: DataType | undefined = column.columnDef.meta?.dataType;
const columnFormatOptions = dataType ? formatOptions[dataType] : [];
const renderFormatOptions = () => {
if (columnFormatOptions.length === 0 || !column.getCanFormat?.()) {
return null;
}
const FormatIcon = DATA_TYPE_ICON[dataType || "unknown"];
const currentFormat = column.getColumnFormatting?.();
return (
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<FormatIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Format
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
{Boolean(currentFormat) && (
<>
<DropdownMenuItem
key={"clear"}
variant={"danger"}
onClick={() => column.setColumnFormatting(undefined)}
>
Clear
</DropdownMenuItem>
<DropdownMenuSeparator />
</>
)}
{columnFormatOptions.map((option) => (
<DropdownMenuItem
key={option}
onClick={() => column.setColumnFormatting(option)}
>
<span
className={cn(currentFormat === option && "font-semibold")}
>
{option}
</span>
<span className="ml-auto pl-5 text-xs text-muted-foreground">
{formattingExample(option)}
</span>
</DropdownMenuItem>
))}
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
);
};
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild={true}>
<div
className={cn(
"group flex items-center my-1 space-between w-full select-none gap-2 border hover:border-border border-transparent hover:bg-[var(--slate-3)] data-[state=open]:bg-[var(--slate-3)] data-[state=open]:border-border rounded px-1 -mx-1",
className,
)}
data-testid="data-table-sort-button"
>
<span className="flex-1">{header}</span>
<span
className={cn(
"h-5 py-1 px-1",
!column.getIsSorted() &&
"invisible group-hover:visible data-[state=open]:visible",
)}
>
{column.getIsSorted() === "desc" ? (
<DescIcon className="h-3 w-3" />
) : column.getIsSorted() === "asc" ? (
<AscIcon className="h-3 w-3" />
) : (
<ChevronsUpDown className="h-3 w-3" />
)}
</span>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="start">
{dtype && (
<>
<div className="flex-1 px-2 text-xs text-muted-foreground font-bold">
{dtype}
</div>
<DropdownMenuSeparator />
</>
)}
{renderSorts()}
{!column.id.startsWith(NAMELESS_COLUMN_PREFIX) && (
<DropdownMenuItem
onClick={async () =>
await copyToClipboard(
typeof header === "string" ? header : column.id,
)
}
>
<CopyIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Copy column name
</DropdownMenuItem>
)}
{renderColumnPinning()}
{renderColumnWrapping()}
{renderFormatOptions()}
<DropdownMenuItemFilter column={column} />
</DropdownMenuContent>
</DropdownMenu>
);
};
export const DataTableColumnHeaderWithSummary = <TData, TValue>({
column,
header,
summary,
className,
}: DataTableColumnHeaderProps<TData, TValue> & {
summary: React.ReactNode;
}) => {
return (
<div
className={cn(
"flex flex-col h-full py-1 justify-between items-start gap-1",
className,
)}
>
<DataTableColumnHeader
column={column}
header={header}
className={className}
/>
{summary}
</div>
);
};
export const DropdownMenuItemFilter = <TData, TValue>({
column,
}: React.PropsWithChildren<{
column: Column<TData, TValue>;
}>) => {
const canFilter = column.getCanFilter();
if (!canFilter) {
return null;
}
const filterType = column.columnDef.meta?.filterType;
if (!filterType) {
return null;
}
const hasFilter = column.getFilterValue() !== undefined;
const filterMenuItem = (
<DropdownMenuSubTrigger>
<FilterIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Filter
</DropdownMenuSubTrigger>
);
const clearFilterMenuItem = (
<DropdownMenuItem onClick={() => column.setFilterValue(undefined)}>
<FilterX className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
Clear filter
</DropdownMenuItem>
);
if (filterType === "boolean") {
return (
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
{filterMenuItem}
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem
onClick={() => column.setFilterValue(Filter.boolean(true))}
>
True
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => column.setFilterValue(Filter.boolean(false))}
>
False
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{hasFilter && clearFilterMenuItem}
</>
);
}
if (filterType === "text") {
return (
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
{filterMenuItem}
<DropdownMenuPortal>
<DropdownMenuSubContent>
<TextFilter column={column} />
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{hasFilter && clearFilterMenuItem}
</>
);
}
if (filterType === "number") {
return (
<>
<DropdownMenuSeparator />
<DropdownMenuSub>
{filterMenuItem}
<DropdownMenuPortal>
<DropdownMenuSubContent>
<NumberRangeFilter column={column} />
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
{hasFilter && clearFilterMenuItem}
</>
);
}
if (filterType === "select") {
// Not implemented
return null;
}
if (filterType === "time") {
// Not implemented
return null;
}
if (filterType === "datetime") {
// Not implemented
return null;
}
if (filterType === "date") {
// Not implemented
return null;
}
logNever(filterType);
return null;
};
const NumberRangeFilter = <TData, TValue>({
column,
}: {
column: Column<TData, TValue>;
}) => {
const currentFilter = column.getFilterValue() as
| ColumnFilterForType<"number">
| undefined;
const hasFilter = currentFilter !== undefined;
const [min, setMin] = useState<number | undefined>(currentFilter?.min);
const [max, setMax] = useState<number | undefined>(currentFilter?.max);
const minRef = useRef<HTMLInputElement>(null);
const maxRef = useRef<HTMLInputElement>(null);
const handleApply = (opts: { min?: number; max?: number } = {}) => {
column.setFilterValue(
Filter.number({
min: opts.min ?? min,
max: opts.max ?? max,
}),
);
};
return (
<div className="flex flex-col gap-1 pt-3 px-2">
<div className="flex gap-1 items-center">
<NumberField
ref={minRef}
value={min}
onChange={(value) => setMin(value)}
placeholder="min"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleApply({ min: Number.parseFloat(e.currentTarget.value) });
}
if (e.key === "Tab") {
maxRef.current?.focus();
}
}}
className="shadow-none! border-border hover:shadow-none!"
/>
<MinusIcon className="h-5 w-5 text-muted-foreground" />
<NumberField
ref={maxRef}
value={max}
onChange={(value) => setMax(value)}
onKeyDown={(e) => {
if (e.key === "Enter") {
handleApply({ max: Number.parseFloat(e.currentTarget.value) });
}
if (e.key === "Tab") {
minRef.current?.focus();
}
}}
placeholder="max"
className="shadow-none! border-border hover:shadow-none!"
/>
</div>
<div className="flex gap-2 px-2 justify-between">
<Button variant="link" size="sm" onClick={() => handleApply()}>
Apply
</Button>
<Button
variant="linkDestructive"
size="sm"
disabled={!hasFilter}
className=""
onClick={() => {
setMin(undefined);
setMax(undefined);
column.setFilterValue(undefined);
}}
>
Clear
</Button>
</div>
</div>
);
};
const TextFilter = <TData, TValue>({
column,
}: {
column: Column<TData, TValue>;
}) => {
const currentFilter = column.getFilterValue() as
| ColumnFilterForType<"text">
| undefined;
const hasFilter = currentFilter !== undefined;
const [value, setValue] = useState<string>(currentFilter?.text ?? "");
const handleApply = () => {
if (value === "") {
column.setFilterValue(undefined);
return;
}
column.setFilterValue(Filter.text(value));
};
return (
<div className="flex flex-col gap-1 pt-3 px-2">
<Input
type="text"
icon={<SearchIcon className="h-3 w-3 text-muted-foreground" />}
value={value ?? ""}
onChange={(e) => setValue(e.target.value)}
placeholder="Search"
onKeyDown={(e) => {
if (e.key === "Enter") {
handleApply();
}
}}
className="shadow-none! border-border hover:shadow-none!"
/>
<div className="flex gap-2 px-2 justify-between">
<Button variant="link" size="sm" onClick={() => handleApply()}>
Apply
</Button>
<Button
variant="linkDestructive"
size="sm"
disabled={!hasFilter}
className=""
onClick={() => {
setValue("");
column.setFilterValue(undefined);
}}
>
Clear
</Button>
</div>
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/column-summary.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React, { useContext } from "react";
import { ColumnChartSpecModel } from "./chart-spec-model";
import { useTheme } from "@/theme/useTheme";
import { prettyNumber, prettyScientificNumber } from "@/utils/numbers";
import { prettyDate } from "@/utils/dates";
import { DelayMount } from "../utils/delay-mount";
import { ChartSkeleton } from "../charts/chart-skeleton";
import { logNever } from "@/utils/assertNever";
import { DatePopover } from "./date-popover";
import { createBatchedLoader } from "@/plugins/impl/vega/batched";
export const ColumnChartContext = React.createContext<
ColumnChartSpecModel<unknown>
>(ColumnChartSpecModel.EMPTY);
interface Props<TData, TValue> {
columnId: string;
}
const LazyVegaLite = React.lazy(() =>
import("react-vega").then((m) => ({ default: m.VegaLite })),
);
// We batch multiple calls to the same URL returning the same promise
// for all calls with the same key.
const batchedLoader = createBatchedLoader();
export const TableColumnSummary = <TData, TValue>({
columnId,
}: Props<TData, TValue>) => {
const chartSpecModel = useContext(ColumnChartContext);
const { theme } = useTheme();
const { spec, type, summary } = chartSpecModel.getHeaderSummary(columnId);
let chart: React.ReactNode = null;
if (spec) {
chart = (
<DelayMount
milliseconds={200}
visibility={true}
rootMargin="200px"
fallback={<ChartSkeleton seed={columnId} width={130} height={60} />}
>
<LazyVegaLite
spec={spec}
width={120}
height={50}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
loader={batchedLoader as any}
style={{ minWidth: "unset", maxHeight: "60px" }}
actions={false}
theme={theme === "dark" ? "dark" : "vox"}
/>
</DelayMount>
);
}
const renderDate = (
date: string | number | null | undefined,
type: "date" | "datetime",
) => {
return (
<DatePopover date={date} type={type}>
{prettyDate(date, type)}
</DatePopover>
);
};
const renderStats = () => {
if (!summary) {
return null;
}
switch (type) {
case "date":
case "datetime":
// Without a chart
if (!spec) {
return (
<div className="flex flex-col whitespace-pre">
<span>min: {renderDate(summary.min, type)}</span>
<span>max: {renderDate(summary.max, type)}</span>
<span>unique: {prettyNumber(summary.unique)}</span>
<span>nulls: {prettyNumber(summary.nulls)}</span>
</div>
);
}
return (
<div className="flex justify-between w-full px-2 whitespace-pre">
<span>{renderDate(summary.min, type)}</span>-
<span>{renderDate(summary.max, type)}</span>
</div>
);
case "integer":
case "number":
// Without a chart
if (!spec) {
return (
<div className="flex flex-col whitespace-pre">
<span>
min:{" "}
{typeof summary.min === "number"
? prettyScientificNumber(summary.min)
: summary.min}
</span>
<span>
max:{" "}
{typeof summary.max === "number"
? prettyScientificNumber(summary.max)
: summary.max}
</span>
<span>unique: {prettyNumber(summary.unique)}</span>
<span>nulls: {prettyNumber(summary.nulls)}</span>
</div>
);
}
if (
typeof summary.min === "number" &&
typeof summary.max === "number"
) {
return (
<div className="flex justify-between w-full px-2 whitespace-pre">
<span>{prettyScientificNumber(summary.min)}</span>
<span>{prettyScientificNumber(summary.max)}</span>
</div>
);
}
return (
<div className="flex justify-between w-full px-2 whitespace-pre">
<span>{summary.min}</span>
<span>{summary.max}</span>
</div>
);
case "boolean":
// Without a chart
if (!spec) {
return (
<div className="flex flex-col whitespace-pre">
<span>true: {prettyNumber(summary.true)}</span>
<span>false: {prettyNumber(summary.false)}</span>
</div>
);
}
if (summary.nulls == null || summary.nulls === 0) {
return null;
}
return (
<div className="flex flex-col whitespace-pre">
<span>nulls: {prettyNumber(summary.nulls)}</span>
</div>
);
case "time":
return null;
case "string":
return (
<div className="flex flex-col whitespace-pre">
<span>unique: {prettyNumber(summary.unique)}</span>
<span>nulls: {prettyNumber(summary.nulls)}</span>
</div>
);
case "unknown":
return (
<div className="flex flex-col whitespace-pre">
<span>nulls: {prettyNumber(summary.nulls)}</span>
</div>
);
default:
logNever(type);
return null;
}
};
return (
<div className="flex flex-col items-center text-xs text-muted-foreground align-end">
{chart}
{renderStats()}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/download-actions.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React from "react";
import { Button } from "../ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "../ui/dropdown-menu";
import { toast } from "../ui/use-toast";
import { downloadByURL } from "@/utils/download";
import { ChevronDownIcon } from "lucide-react";
export interface DownloadActionProps {
downloadAs: (req: { format: "csv" | "json" }) => Promise<string>;
}
const options = [
{ label: "CSV", format: "csv" },
{ label: "JSON", format: "json" },
] as const;
export const DownloadAs: React.FC<DownloadActionProps> = (props) => {
const button = (
<Button data-testid="download-as-button" size="xs" variant="link">
Download <ChevronDownIcon className="w-3 h-3 ml-1" />
</Button>
);
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger asChild={true}>{button}</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" className="no-print">
{options.map((option) => (
<DropdownMenuItem
key={option.label}
onSelect={async () => {
const downloadUrl = await props
.downloadAs({
format: option.format,
})
.catch((error) => {
toast({
title: "Failed to download",
description:
"message" in error ? error.message : String(error),
});
});
if (!downloadUrl) {
return;
}
downloadByURL(downloadUrl, "download");
}}
>
{option.label}
</DropdownMenuItem>
))}
</DropdownMenuContent>
</DropdownMenu>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/date-popover.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React from "react";
import { Tooltip } from "@/components/ui/tooltip";
interface DatePopoverProps {
date: Date | string | number | null | undefined;
type: "date" | "datetime";
children: React.ReactNode;
}
export const DatePopover: React.FC<DatePopoverProps> = ({
date,
type,
children,
}) => {
// Return just the text if date is invalid
if (!date || Number.isNaN(new Date(date).getTime())) {
return children;
}
const dateObj = new Date(date);
const relativeTime = getRelativeTime(dateObj);
const content = (
<div className="min-w-[240px] p-1 text-sm">
<div className="text-muted-foreground mb-2">{relativeTime}</div>
<div className="space-y-1">
{type === "datetime" ? (
Object.entries(getTimezones(dateObj)).map(
([timezone, formattedDate]) => (
<div
key={timezone}
className="grid grid-cols-[fit-content(40px),1fr] gap-4 items-center justify-items-end"
>
<span className="bg-muted rounded-md py-1 px-2 w-fit ml-auto">
{timezone}
</span>
<span>{formattedDate}</span>
</div>
),
)
) : (
<span>
{dateObj.toLocaleDateString("en-US", {
timeZone: "UTC",
dateStyle: "long",
})}
</span>
)}
</div>
</div>
);
return (
<Tooltip content={content} delayDuration={200}>
<span>{children}</span>
</Tooltip>
);
};
function getTimezones(date: Date) {
const localTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const hasSubSeconds = date.getUTCMilliseconds() !== 0;
if (hasSubSeconds) {
return {
UTC: new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
}).format(date),
[localTimezone]: new Intl.DateTimeFormat("en-US", {
timeZone: localTimezone,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
fractionalSecondDigits: 3,
}).format(date),
};
}
return {
UTC: new Intl.DateTimeFormat("en-US", {
timeZone: "UTC",
dateStyle: "long",
timeStyle: "medium",
}).format(date),
[localTimezone]: new Intl.DateTimeFormat("en-US", {
timeZone: localTimezone,
dateStyle: "long",
timeStyle: "medium",
}).format(date),
};
}
/**
* Converts a date into a human-readable relative time string (e.g. "2 hours ago", "in 5 minutes")
* Uses Intl.RelativeTimeFormat for localized formatting
*/
function getRelativeTime(date: Date): string {
// Initialize relative time formatter with English locale and "auto" numeric style
// "auto" allows for strings like "yesterday" instead of "1 day ago"
const relativeTimeFormatter = new Intl.RelativeTimeFormat("en", {
numeric: "auto",
});
const currentTime = new Date();
// Get difference in seconds between now and the input date
const differenceInSeconds = (currentTime.getTime() - date.getTime()) / 1000;
// Define time units with their thresholds and conversion factors
// Format: [threshold before next unit, seconds in this unit, unit name]
const timeUnits: Array<[number, number, string]> = [
[60, 1, "second"], // Less than 60 seconds
[60, 60, "minute"], // Less than 60 minutes
[24, 3600, "hour"], // Less than 24 hours
[365, 86_400, "day"], // Less than 365 days
[Number.POSITIVE_INFINITY, 31_536_000, "year"], // Everything else in years
];
// Find the appropriate unit to use
for (const [unitThreshold, secondsInUnit, unitName] of timeUnits) {
const valueInUnits = differenceInSeconds / secondsInUnit;
if (valueInUnits < unitThreshold) {
// Convert to fixed decimal and negate since RelativeTimeFormat expects
// negative values for past times and positive for future times
const roundedValue = -Number(valueInUnits.toFixed(1));
return relativeTimeFormatter.format(
roundedValue,
unitName as Intl.RelativeTimeFormatUnit,
);
}
}
// Should never reach here due to Infinity threshold, but provide fallback
return relativeTimeFormatter.format(0, "second");
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/columns.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import type { ColumnDef } from "@tanstack/react-table";
import {
DataTableColumnHeader,
DataTableColumnHeaderWithSummary,
} from "./column-header";
import { Checkbox } from "../ui/checkbox";
import { MimeCell } from "./mime-cell";
import type { DataType } from "@/core/kernel/messages";
import { TableColumnSummary } from "./column-summary";
import type { FilterType } from "./filters";
import type { FieldTypesWithExternalType } from "./types";
import { UrlDetector } from "./url-detector";
import { cn } from "@/utils/cn";
import { uniformSample } from "./uniformSample";
import { DatePopover } from "./date-popover";
import { Objects } from "@/utils/objects";
import { Maps } from "@/utils/maps";
import { exactDateTime } from "@/utils/dates";
function inferDataType(value: unknown): [type: DataType, displayType: string] {
if (typeof value === "string") {
return ["string", "string"];
}
if (typeof value === "number") {
return ["number", "number"];
}
if (value instanceof Date) {
return ["datetime", "datetime"];
}
if (typeof value === "boolean") {
return ["boolean", "boolean"];
}
if (value == null) {
return ["unknown", "object"];
}
return ["unknown", "object"];
}
export function inferFieldTypes<T>(items: T[]): FieldTypesWithExternalType {
// No items
if (items.length === 0) {
return [];
}
// Not an object
if (typeof items[0] !== "object") {
return [];
}
const fieldTypes: Record<string, [DataType, string]> = {};
// This can be slow for large datasets,
// so only sample 10 evenly distributed rows
uniformSample(items, 10).forEach((item) => {
if (typeof item !== "object") {
return;
}
// We will be a bit defensive and assume values are not homogeneous.
// If any is a mimetype, then we will treat it as a mimetype (i.e. not sortable)
Object.entries(item as object).forEach(([key, value], idx) => {
const currentValue = fieldTypes[key];
if (!currentValue) {
// Set for the first time
fieldTypes[key] = inferDataType(value);
}
// If its not null, override the type
if (value != null) {
// This can be lossy as we infer take the last seen type
fieldTypes[key] = inferDataType(value);
}
});
});
return Objects.entries(fieldTypes);
}
export const NAMELESS_COLUMN_PREFIX = "__m_column__";
export function generateColumns<T>({
rowHeaders,
selection,
fieldTypes,
textJustifyColumns,
wrappedColumns,
showDataTypes,
}: {
rowHeaders: string[];
selection: "single" | "multi" | null;
fieldTypes: FieldTypesWithExternalType;
textJustifyColumns?: Record<string, "left" | "center" | "right">;
wrappedColumns?: string[];
showDataTypes?: boolean;
}): Array<ColumnDef<T>> {
const rowHeadersSet = new Set(rowHeaders);
const typesByColumn = Maps.keyBy(fieldTypes, (entry) => entry[0]);
const getMeta = (key: string) => {
const types = typesByColumn.get(key)?.[1];
const isRowHeader = rowHeadersSet.has(key);
if (isRowHeader || !types) {
return {
rowHeader: isRowHeader,
};
}
return {
rowHeader: isRowHeader,
filterType: getFilterTypeForFieldType(types[0]),
dtype: types[1],
dataType: types[0],
};
};
const columnKeys = [
...rowHeaders,
...fieldTypes.map(([columnName]) => columnName),
];
const columns = columnKeys.map(
(key, idx): ColumnDef<T> => ({
id: key || `${NAMELESS_COLUMN_PREFIX}${idx}`,
// Use an accessorFn instead of an accessorKey because column names
// may have periods in them ...
// https://github.com/TanStack/table/issues/1671
accessorFn: (row) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (row as any)[key];
},
header: ({ column }) => {
const dtype = column.columnDef.meta?.dtype;
const headerWithType = (
<div className="flex flex-col">
<span className="font-bold">{key}</span>
{showDataTypes && dtype && (
<span className="text-xs text-muted-foreground">{dtype}</span>
)}
</div>
);
// Row headers have no summaries
if (rowHeadersSet.has(key)) {
return (
<DataTableColumnHeader header={headerWithType} column={column} />
);
}
return (
<DataTableColumnHeaderWithSummary
key={key}
header={headerWithType}
column={column}
summary={<TableColumnSummary columnId={key} />}
/>
);
},
cell: ({ column, renderValue, getValue }) => {
// Row headers are bold
if (rowHeadersSet.has(key)) {
return <b>{String(renderValue())}</b>;
}
const value = getValue();
const justify = textJustifyColumns?.[key];
const wrapped = wrappedColumns?.includes(key);
const format = column.getColumnFormatting?.();
if (format) {
return (
<div className={getCellStyleClass(justify, wrapped)}>
{column.applyColumnFormatting(value)}
</div>
);
}
if (isPrimitiveOrNullish(value)) {
const rendered = renderValue();
return (
<div className={getCellStyleClass(justify, wrapped)}>
{rendered == null ? (
""
) : typeof rendered === "string" ? (
<UrlDetector text={rendered} />
) : (
String(rendered)
)}
</div>
);
}
if (value instanceof Date) {
// e.g. 2010-10-07 17:15:00
const type =
column.columnDef.meta?.dataType === "date" ? "date" : "datetime";
return (
<div className={getCellStyleClass(justify, wrapped)}>
<DatePopover date={value} type={type}>
{exactDateTime(value)}
</DatePopover>
</div>
);
}
return (
<div className={getCellStyleClass(justify, wrapped)}>
<MimeCell value={value} />
</div>
);
},
// Remove any default filtering
filterFn: undefined,
// Can only sort if key is defined
// For example, unnamed index columns, won't be sortable
enableSorting: !!key,
meta: getMeta(key),
}),
);
if (selection === "single" || selection === "multi") {
columns.unshift({
id: "__select__",
maxSize: 40,
header: ({ table }) =>
selection === "multi" ? (
<Checkbox
data-testid="select-all-checkbox"
checked={table.getIsAllPageRowsSelected()}
onCheckedChange={(value) =>
table.toggleAllPageRowsSelected(!!value)
}
aria-label="Select all"
className="mx-1.5 my-4"
/>
) : null,
cell: ({ row }) => (
<Checkbox
data-testid="select-row-checkbox"
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
className="mx-2"
/>
),
enableSorting: false,
enableHiding: false,
});
}
return columns;
}
function isPrimitiveOrNullish(value: unknown): boolean {
if (value == null) {
return true;
}
const isObject = typeof value === "object";
return !isObject;
}
function getFilterTypeForFieldType(
type: DataType | undefined,
): FilterType | undefined {
if (type === undefined) {
return undefined;
}
switch (type) {
case "string":
return "text";
case "number":
return "number";
case "integer":
return "number";
case "date":
return "date";
case "datetime":
return "datetime";
case "time":
return "time";
case "boolean":
return "boolean";
default:
return undefined;
}
}
function getCellStyleClass(
justify: "left" | "center" | "right" | undefined,
wrapped: boolean | undefined,
): string {
return cn(
"w-full",
"text-left",
justify === "center" && "text-center",
justify === "right" && "text-right",
wrapped && "whitespace-pre-wrap min-w-[200px] break-words",
);
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/filter-pills.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import type {
ColumnFilter,
ColumnFiltersState,
Table,
} from "@tanstack/react-table";
import { Badge } from "../ui/badge";
import type { ColumnFilterValue } from "./filters";
import { logNever } from "@/utils/assertNever";
import { XIcon } from "lucide-react";
interface Props<TData> {
filters: ColumnFiltersState | undefined;
table: Table<TData>;
}
export const FilterPills = <TData,>({ filters, table }: Props<TData>) => {
if (!filters || filters.length === 0) {
return null;
}
function renderFilterPill(filter: ColumnFilter) {
const formattedValue = formatValue(filter.value as ColumnFilterValue);
if (!formattedValue) {
return null;
}
return (
<Badge key={filter.id} variant="secondary">
{filter.id} {formattedValue}{" "}
<span
className="cursor-pointer opacity-60 hover:opacity-100 pl-1 py-[2px]"
onClick={() => {
table.setColumnFilters((filters) =>
filters.filter((f) => f.id !== filter.id),
);
}}
>
<XIcon className="w-3.5 h-3.5" />
</span>
</Badge>
);
}
return (
<div className="flex flex-wrap gap-2 px-1">
{filters.map(renderFilterPill)}
</div>
);
};
function formatValue(value: ColumnFilterValue) {
if (!("type" in value)) {
return;
}
if (value.type === "number") {
return formatMinMax(value.min, value.max);
}
if (value.type === "date") {
return formatMinMax(value.min?.toISOString(), value.max?.toISOString());
}
if (value.type === "time") {
const formatTime = new Intl.DateTimeFormat("en-US", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
return formatMinMax(
value.min ? formatTime.format(value.min) : undefined,
value.max ? formatTime.format(value.max) : undefined,
);
}
if (value.type === "datetime") {
return formatMinMax(value.min?.toISOString(), value.max?.toISOString());
}
if (value.type === "boolean") {
return `is ${value.value ? "True" : "False"}`;
}
if (value.type === "select") {
return `is in [${value.options.join(", ")}]`;
}
if (value.type === "text") {
return `contains "${value.text}"`;
}
logNever(value);
return undefined;
}
function formatMinMax(
min: string | number | undefined,
max: string | number | undefined,
) {
if (min === undefined && max === undefined) {
return;
}
if (min === max) {
return `== ${min}`;
}
if (min === undefined) {
return `<= ${max}`;
}
if (max === undefined) {
return `>= ${min}`;
}
return `${min} - ${max}`;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/data-table.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
// tanstack/table is not compatible with React compiler
// https://github.com/TanStack/table/issues/5567
import React, { memo } from "react";
import {
type ColumnDef,
type ColumnFiltersState,
ColumnPinning,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
type OnChangeFn,
type PaginationState,
type RowSelectionState,
type SortingState,
useReactTable,
} from "@tanstack/react-table";
import { Table } from "@/components/ui/table";
import type { DownloadActionProps } from "./download-actions";
import { cn } from "@/utils/cn";
import { FilterPills } from "./filter-pills";
import { useColumnPinning } from "./hooks/useColumnPinning";
import { renderTableHeader, renderTableBody } from "./renderers";
import { SearchBar } from "./SearchBar";
import { TableActions } from "./TableActions";
import { ColumnFormattingFeature } from "./column-formatting/feature";
import { ColumnWrappingFeature } from "./column-wrapping/feature";
interface DataTableProps<TData> extends Partial<DownloadActionProps> {
wrapperClassName?: string;
className?: string;
columns: Array<ColumnDef<TData>>;
data: TData[];
// Sorting
manualSorting?: boolean; // server-side sorting
sorting?: SortingState; // controlled sorting
setSorting?: OnChangeFn<SortingState>; // controlled sorting
// Pagination
totalRows: number | "too_many";
totalColumns: number;
pagination?: boolean;
manualPagination?: boolean; // server-side pagination
paginationState?: PaginationState; // controlled pagination
setPaginationState?: OnChangeFn<PaginationState>; // controlled pagination
// Selection
selection?: "single" | "multi" | null;
rowSelection?: RowSelectionState;
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
// Search
enableSearch?: boolean;
searchQuery?: string;
onSearchQueryChange?: (query: string) => void;
showFilters?: boolean;
filters?: ColumnFiltersState;
onFiltersChange?: OnChangeFn<ColumnFiltersState>;
reloading?: boolean;
// Columns
freezeColumnsLeft?: string[];
freezeColumnsRight?: string[];
}
const DataTableInternal = <TData,>({
wrapperClassName,
className,
columns,
data,
selection,
totalColumns,
totalRows,
manualSorting = false,
sorting,
setSorting,
rowSelection,
paginationState,
setPaginationState,
downloadAs,
manualPagination = false,
pagination = false,
onRowSelectionChange,
enableSearch = false,
searchQuery,
onSearchQueryChange,
showFilters = false,
filters,
onFiltersChange,
reloading,
freezeColumnsLeft,
freezeColumnsRight,
}: DataTableProps<TData>) => {
const [isSearchEnabled, setIsSearchEnabled] = React.useState<boolean>(false);
const { columnPinning, setColumnPinning } = useColumnPinning(
freezeColumnsLeft,
freezeColumnsRight,
);
const table = useReactTable<TData>({
_features: [ColumnPinning, ColumnWrappingFeature, ColumnFormattingFeature],
data,
columns,
getCoreRowModel: getCoreRowModel(),
// pagination
rowCount: totalRows === "too_many" ? undefined : totalRows,
...(setPaginationState
? {
onPaginationChange: setPaginationState,
getRowId: (_row, idx) => {
if (!paginationState) {
return String(idx);
}
// Add offset if manualPagination is enabled
const offset = manualPagination
? paginationState.pageIndex * paginationState.pageSize
: 0;
return String(idx + offset);
},
}
: {}),
manualPagination: manualPagination,
getPaginationRowModel: getPaginationRowModel(),
// sorting
...(setSorting ? { onSortingChange: setSorting } : {}),
manualSorting: manualSorting,
getSortedRowModel: getSortedRowModel(),
// filtering
manualFiltering: true,
enableColumnFilters: showFilters,
getFilteredRowModel: getFilteredRowModel(),
onColumnFiltersChange: onFiltersChange,
// selection
onRowSelectionChange: onRowSelectionChange,
state: {
...(sorting ? { sorting } : {}),
columnFilters: filters,
...// Controlled state
(paginationState
? { pagination: paginationState }
: // Uncontrolled state
pagination && !paginationState
? {}
: // No pagination, show all rows
{ pagination: { pageIndex: 0, pageSize: data.length } }),
rowSelection,
columnPinning: columnPinning,
},
onColumnPinningChange: setColumnPinning,
});
return (
<div className={cn(wrapperClassName, "flex flex-col space-y-1")}>
<FilterPills filters={filters} table={table} />
<div className={cn(className || "rounded-md border overflow-hidden")}>
{onSearchQueryChange && enableSearch && (
<SearchBar
value={searchQuery || ""}
onHide={() => setIsSearchEnabled(false)}
handleSearch={onSearchQueryChange}
hidden={!isSearchEnabled}
reloading={reloading}
/>
)}
<Table>
{renderTableHeader(table)}
{renderTableBody(table, columns)}
</Table>
</div>
<TableActions
enableSearch={enableSearch}
totalColumns={totalColumns}
onSearchQueryChange={onSearchQueryChange}
isSearchEnabled={isSearchEnabled}
setIsSearchEnabled={setIsSearchEnabled}
pagination={pagination}
selection={selection}
onRowSelectionChange={onRowSelectionChange}
table={table}
downloadAs={downloadAs}
/>
</div>
);
};
export const DataTable = memo(DataTableInternal) as typeof DataTableInternal;
```
File: /Users/morganmcguire/ML/marimo/frontend/public/circle-check.ico
```ico
00®(0` ]tWwVxVxWxWxVyXwWwUwWxWxXxWzWxVyVyWwWwWxXxWxXxWxˇˇˇ  





 

 



  

  







  

  



 

 





  ˇˇˇˇˇˇˇˇˇˇˇˇˇˇÄˇˇˇ˛ˇˇ¯ˇˇ‡ˇˇ¿ˇˇÄˇˇˇ˛¸?¯¯‡‡¿¿¿¿¿¿¿¿¿¿¿¿¿¿‡‡¯¯¸?˛ˇˇˇÄˇˇ¿ˇˇ‡ˇˇ¯ˇˇ˛ˇˇˇÄˇˇˇˇˇˇˇˇˇˇˇˇˇˇ
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/filters.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import type { DataType } from "@/core/kernel/messages";
import type { ConditionType } from "@/plugins/impl/data-frames/schema";
import type { ColumnId } from "@/plugins/impl/data-frames/types";
import { assertNever } from "@/utils/assertNever";
import type { RowData } from "@tanstack/react-table";
declare module "@tanstack/react-table" {
//allows us to define custom properties for our columns
interface ColumnMeta<TData extends RowData, TValue> {
rowHeader?: boolean;
dtype?: string;
dataType?: DataType;
filterType?: FilterType;
}
}
export type FilterType =
| "text"
| "number"
| "date"
| "datetime"
| "time"
| "select"
| "boolean";
// Filter is a factory function that creates a filter object
export const Filter = {
number(opts: { min?: number; max?: number }) {
return {
type: "number",
...opts,
} as const;
},
text(text: string) {
return {
type: "text",
text,
} as const;
},
date(opts: { min?: Date; max?: Date }) {
return {
type: "date",
...opts,
} as const;
},
datetime(opts: { min?: Date; max?: Date }) {
return {
type: "datetime",
...opts,
} as const;
},
time(opts: { min?: Date; max?: Date }) {
return {
type: "time",
...opts,
} as const;
},
boolean(value: boolean) {
return {
type: "boolean",
value,
} as const;
},
select(options: string[]) {
return {
type: "select",
options,
} as const;
},
};
export type ColumnFilterValue = ReturnType<
(typeof Filter)[keyof typeof Filter]
>;
export type ColumnFilterForType<T extends FilterType> = T extends FilterType
? Extract<ColumnFilterValue, { type: T }>
: never;
export function filterToFilterCondition(
columnIdString: string,
filter: ColumnFilterValue | undefined,
): ConditionType[] | ConditionType {
if (!filter) {
return [];
}
const columnId = columnIdString as ColumnId;
switch (filter.type) {
case "number": {
const conditions: ConditionType[] = [];
if (filter.min !== undefined) {
conditions.push({
column_id: columnId,
operator: ">=",
value: filter.min,
});
}
if (filter.max !== undefined) {
conditions.push({
column_id: columnId,
operator: "<=",
value: filter.max,
});
}
return conditions;
}
case "text":
return {
column_id: columnId,
operator: "contains",
value: filter.text,
};
case "datetime": {
const conditions: ConditionType[] = [];
if (filter.min !== undefined) {
conditions.push({
column_id: columnId,
operator: ">=",
value: filter.min.toISOString(),
});
}
if (filter.max !== undefined) {
conditions.push({
column_id: columnId,
operator: "<=",
value: filter.max.toISOString(),
});
}
return conditions;
}
case "date": {
const conditions: ConditionType[] = [];
if (filter.min !== undefined) {
conditions.push({
column_id: columnId,
operator: ">=",
value: filter.min.toISOString(),
});
}
if (filter.max !== undefined) {
conditions.push({
column_id: columnId,
operator: "<=",
value: filter.max.toISOString(),
});
}
return conditions;
}
case "time": {
const conditions: ConditionType[] = [];
if (filter.min !== undefined) {
conditions.push({
column_id: columnId,
operator: ">=",
value: filter.min.toISOString(),
});
}
if (filter.max !== undefined) {
conditions.push({
column_id: columnId,
operator: "<=",
value: filter.max.toISOString(),
});
}
return conditions;
}
case "boolean":
if (filter.value) {
return {
column_id: columnId,
operator: "is_true",
};
}
if (!filter.value) {
return {
column_id: columnId,
operator: "is_false",
};
}
return [];
case "select":
return {
column_id: columnId,
operator: "in",
value: filter.options,
};
default:
assertNever(filter);
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/loading-table.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/utils/cn";
interface Props {
wrapperClassName?: string;
className?: string;
pageSize?: number;
}
export const LoadingTable = ({
wrapperClassName,
className,
pageSize = 10,
}: Props) => {
const NUM_COLUMNS = 8;
return (
<div className={cn(wrapperClassName, "flex flex-col space-y-2")}>
<div className={cn(className || "rounded-md border")}>
<Table>
<TableHeader>
{Array.from({ length: 1 }).map((_, i) => (
<TableRow key={i}>
{Array.from({ length: NUM_COLUMNS }).map((_, j) => (
<TableHead key={j}>
<div className="h-4 bg-[var(--slate-5)] animate-pulse rounded-md w-[70%]" />
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{Array.from({ length: pageSize }).map((_, i) => (
<TableRow key={i}>
{Array.from({ length: NUM_COLUMNS }).map((_, j) => (
<TableCell key={j}>
<div className="h-4 bg-[var(--slate-5)] animate-pulse rounded-md w-[90%]" />
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</div>
<div className="flex align-items justify-between flex-shrink-0 h-8" />
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/pagination.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import type { Table } from "@tanstack/react-table";
import {
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { PluralWord } from "@/utils/pluralize";
import { range } from "lodash-es";
interface DataTablePaginationProps<TData> {
table: Table<TData>;
selection?: "single" | "multi" | null;
totalColumns: number;
onSelectAllRowsChange?: (value: boolean) => void;
}
export const DataTablePagination = <TData,>({
table,
selection,
onSelectAllRowsChange,
totalColumns,
}: DataTablePaginationProps<TData>) => {
const renderTotal = () => {
const selected = Object.keys(table.getState().rowSelection).length;
const isAllPageSelected = table.getIsAllPageRowsSelected();
const numRows = table.getRowCount();
const isAllSelected = selected === numRows;
if (isAllPageSelected && !isAllSelected) {
return (
<>
<span>{prettyNumber(selected)} selected</span>
<Button
size="xs"
data-testid="select-all-button"
variant="link"
className="h-4"
onClick={() => {
if (onSelectAllRowsChange) {
onSelectAllRowsChange(true);
} else {
table.toggleAllRowsSelected(true);
}
}}
>
Select all {prettyNumber(numRows)}
</Button>
</>
);
}
if (selected) {
return (
<>
<span>{prettyNumber(selected)} selected</span>
<Button
size="xs"
data-testid="clear-selection-button"
variant="link"
className="h-4"
onClick={() => {
if (onSelectAllRowsChange) {
onSelectAllRowsChange(false);
} else {
table.toggleAllRowsSelected(false);
}
}}
>
Clear selection
</Button>
</>
);
}
const rowsLabel = `${prettyNumber(numRows)} ${new PluralWord("row").pluralize(numRows)}`;
const columnsLabel = `${prettyNumber(totalColumns)} ${new PluralWord("column").pluralize(totalColumns)}`;
return <span>{[rowsLabel, columnsLabel].join(", ")}</span>;
};
const currentPage = Math.min(
table.getState().pagination.pageIndex + 1,
table.getPageCount(),
);
const totalPages = table.getPageCount();
return (
<div className="flex flex-1 items-center justify-between px-2">
<div className="text-sm text-muted-foreground">{renderTotal()}</div>
<div className="flex items-end space-x-2">
<Button
size="xs"
variant="outline"
data-testid="first-page-button"
className="hidden h-6 w-6 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to first page</span>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
size="xs"
variant="outline"
data-testid="previous-page-button"
className="h-6 w-6 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Go to previous page</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<div className="flex items-center justify-center text-xs font-medium gap-1">
<span>Page</span>
<PageSelector
currentPage={currentPage}
totalPages={totalPages}
onPageChange={(page) => table.setPageIndex(page)}
/>
<span className="flex-shrink-0">of {prettyNumber(totalPages)}</span>
</div>
<Button
size="xs"
variant="outline"
data-testid="next-page-button"
className="h-6 w-6 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to next page</span>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
size="xs"
variant="outline"
data-testid="last-page-button"
className="hidden h-6 w-6 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Go to last page</span>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
);
};
function prettyNumber(value: number): string {
return new Intl.NumberFormat().format(value);
}
export const PageSelector = ({
currentPage,
totalPages,
onPageChange,
}: {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}) => {
const renderOption = (i: number) => (
<option key={i} value={i + 1}>
{i + 1}
</option>
);
const renderEllipsis = (key: number) => (
<option key={`__${key}__`} disabled={true} value={`__${key}__`}>
...
</option>
);
const renderPageOptions = () => {
/* If this is too large, this can cause the browser to hang. */
if (totalPages <= 100) {
return range(totalPages).map((i) => renderOption(i));
}
const middle = Math.floor(totalPages / 2);
// Show the first 10 pages, the middle 10 pages, and the last 10 pages.
const firstPages = range(10).map((i) => renderOption(i));
const middlePages = range(10).map((i) => renderOption(middle - 5 + i));
const lastPages = range(10).map((i) => renderOption(totalPages - 10 + i));
const result = [
...firstPages,
renderEllipsis(1),
...middlePages,
renderEllipsis(2),
...lastPages,
];
if (currentPage > 10 && currentPage <= middle - 5) {
result.splice(
10,
1, // delete the first ellipsis
renderEllipsis(1),
renderOption(currentPage - 1),
renderEllipsis(11),
);
}
if (currentPage > middle + 5 && currentPage <= totalPages - 10) {
result.splice(
-11,
1, // delete the first ellipsis
renderEllipsis(2),
renderOption(currentPage - 1),
renderEllipsis(22),
);
}
return result;
};
return (
<select
className="cursor-pointer border rounded"
value={currentPage}
data-testid="page-select"
onChange={(e) => onPageChange(Number(e.target.value) - 1)}
>
{renderPageOptions()}
</select>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/types.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import type { DataType } from "@/core/kernel/messages";
import { Objects } from "@/utils/objects";
export interface ColumnHeaderSummary {
column: string | number;
min?: number | string | undefined | null;
max?: number | string | undefined | null;
unique?: number | unknown[] | undefined | null;
nulls?: number | null;
true?: number | null;
false?: number | null;
}
export type FieldTypesWithExternalType = Array<
[columnName: string, [dataType: DataType, externalType: string]]
>;
export type FieldTypes = Record<string, DataType>;
export function toFieldTypes(
fieldTypes: FieldTypesWithExternalType,
): FieldTypes {
return Objects.collect(
fieldTypes,
([columnName]) => columnName,
([, [type]]) => type,
);
}
export const SELECT_COLUMN_ID = "__select__";
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/mime-cell.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { cn } from "@/utils/cn";
import { OutputRenderer } from "../editor/Output";
import type { OutputMessage } from "@/core/kernel/messages";
interface MimeCellProps {
value: unknown;
}
export const MimeCell = ({ value }: MimeCellProps) => {
if (typeof value !== "object" || value === null) {
return null;
}
if (!("mimetype" in value && "data" in value)) {
return null;
}
const message = {
channel: "output",
data: value.data,
mimetype: value.mimetype,
timestamp: 0,
} as OutputMessage;
return (
<div className={cn("flex items-center space-x-2")}>
<OutputRenderer message={message} />
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/renderers.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import {
TableHeader,
TableHead,
TableBody,
TableRow,
TableCell,
} from "@/components/ui/table";
import {
flexRender,
type Table,
type ColumnDef,
type Row,
type Column,
type Table as TanStackTable,
type HeaderGroup,
type Cell,
} from "@tanstack/react-table";
import { cn } from "@/utils/cn";
export function renderTableHeader<TData>(
table: Table<TData>,
): JSX.Element | null {
if (!table.getRowModel().rows?.length) {
return null;
}
const renderHeaderGroup = (headerGroups: Array<HeaderGroup<TData>>) => {
return headerGroups.map((headerGroup) =>
headerGroup.headers.map((header) => {
const { className, style } = getPinningStyles(header.column);
return (
<TableHead
key={header.id}
className={cn(
"h-auto min-h-10 whitespace-pre align-top",
className,
)}
style={style}
ref={(thead) => columnSizingHandler(thead, table, header.column)}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
);
}),
);
};
return (
<TableHeader>
{renderHeaderGroup(table.getLeftHeaderGroups())}
{renderHeaderGroup(table.getCenterHeaderGroups())}
{renderHeaderGroup(table.getRightHeaderGroups())}
</TableHeader>
);
}
export function renderTableBody<TData>(
table: Table<TData>,
columns: Array<ColumnDef<TData>>,
): JSX.Element {
const renderCells = (row: Row<TData>, cells: Array<Cell<TData, unknown>>) => {
return cells.map((cell) => {
const { className, style } = getPinningStyles(cell.column);
return (
<TableCell
key={cell.id}
className={cn(
"whitespace-pre truncate max-w-[300px]",
cell.column.getColumnWrapping &&
cell.column.getColumnWrapping() === "wrap" &&
"whitespace-pre-wrap min-w-[200px]",
className,
)}
style={style}
title={String(cell.getValue())}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
});
};
return (
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
onClick={() => {
if (table.getIsSomeRowsSelected()) {
row.toggleSelected();
}
}}
>
{renderCells(row, row.getLeftVisibleCells())}
{renderCells(row, row.getCenterVisibleCells())}
{renderCells(row, row.getRightVisibleCells())}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
);
}
function getPinningStyles<TData>(
column: Column<TData>,
): React.HTMLAttributes<HTMLElement> {
const isPinned = column.getIsPinned();
const isLastLeftPinnedColumn =
isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinnedColumn =
isPinned === "right" && column.getIsFirstColumn("right");
return {
className: cn(isPinned && "bg-inherit", "shadow-r z-10"),
style: {
boxShadow:
isLastLeftPinnedColumn && column.id !== "__select__"
? "-4px 0 4px -4px var(--slate-8) inset"
: isFirstRightPinnedColumn
? "4px 0 4px -4px var(--slate-8) inset"
: undefined,
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
opacity: 1,
position: isPinned ? "sticky" : "relative",
zIndex: isPinned ? 1 : 0,
width: column.getSize(),
},
};
}
// Update column sizes in table state for column pinning offsets
// https://github.com/TanStack/table/discussions/3947#discussioncomment-9564867
function columnSizingHandler<TData>(
thead: HTMLTableCellElement | null,
table: TanStackTable<TData>,
column: Column<TData>,
) {
if (!thead) {
return;
}
if (
table.getState().columnSizing[column.id] ===
thead.getBoundingClientRect().width
) {
return;
}
table.setColumnSizing((prevSizes) => ({
...prevSizes,
[column.id]: thead.getBoundingClientRect().width,
}));
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/TableActions.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
"use no memo";
import React from "react";
import { Tooltip } from "../ui/tooltip";
import { Button } from "../ui/button";
import { SearchIcon } from "lucide-react";
import { DataTablePagination } from "./pagination";
import { DownloadAs, type DownloadActionProps } from "./download-actions";
import type { Table, RowSelectionState } from "@tanstack/react-table";
interface TableActionsProps<TData> {
enableSearch: boolean;
onSearchQueryChange?: (query: string) => void;
isSearchEnabled: boolean;
setIsSearchEnabled: React.Dispatch<React.SetStateAction<boolean>>;
pagination: boolean;
totalColumns: number;
selection?: "single" | "multi" | null;
onRowSelectionChange?: (value: RowSelectionState) => void;
table: Table<TData>;
downloadAs?: DownloadActionProps["downloadAs"];
}
export const TableActions = <TData,>({
enableSearch,
onSearchQueryChange,
isSearchEnabled,
setIsSearchEnabled,
pagination,
totalColumns,
selection,
onRowSelectionChange,
table,
downloadAs,
}: TableActionsProps<TData>) => {
return (
<div className="flex items-center justify-between flex-shrink-0 pt-1">
{onSearchQueryChange && enableSearch && (
<Tooltip content="Search">
<Button
variant="text"
size="xs"
className="mb-0"
onClick={() => setIsSearchEnabled(!isSearchEnabled)}
>
<SearchIcon className="w-4 h-4 text-muted-foreground" />
</Button>
</Tooltip>
)}
{pagination ? (
<DataTablePagination
totalColumns={totalColumns}
selection={selection}
onSelectAllRowsChange={
onRowSelectionChange
? (value: boolean) => {
if (value) {
const allKeys = Array.from(
{ length: table.getRowModel().rows.length },
(_, i) => [i, true] as const,
);
onRowSelectionChange(Object.fromEntries(allKeys));
} else {
onRowSelectionChange({});
}
}
: undefined
}
table={table}
/>
) : (
<div />
)}
{downloadAs && <DownloadAs downloadAs={downloadAs} />}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/uniformSample.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
/**
* Uniformly sample n items from an array
*/
export function uniformSample<T>(items: T[], n: number): T[] {
if (items.length <= n) {
return items;
}
const sample: T[] = [];
const step = items.length / n;
for (let i = 0; i < n - 1; i++) {
const idx = Math.floor(i * step);
sample.push(items[idx]);
}
const last = items.at(-1) as T;
sample.push(last);
return sample;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/SearchBar.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React, { useState, useEffect } from "react";
import { SearchIcon } from "lucide-react";
import { Spinner } from "../icons/spinner";
import { cn } from "@/utils/cn";
import { useDebounce } from "@uidotdev/usehooks";
import useEvent from "react-use-event-hook";
interface SearchBarProps {
hidden: boolean;
value: string;
handleSearch: (query: string) => void;
onHide: () => void;
reloading?: boolean;
}
export const SearchBar = ({
hidden,
value,
handleSearch,
onHide,
reloading,
}: SearchBarProps) => {
const [internalValue, setInternalValue] = useState(value);
const debouncedSearch = useDebounce(internalValue, 500);
const onSearch = useEvent(handleSearch);
const ref = React.useRef<HTMLInputElement>(null);
useEffect(() => {
onSearch(debouncedSearch);
}, [debouncedSearch, onSearch]);
useEffect(() => {
if (hidden) {
setInternalValue("");
} else {
ref.current?.focus();
}
}, [hidden]);
return (
<div
className={cn(
"flex items-center space-x-2 h-8 px-2 border-b transition-all overflow-hidden duration-300 opacity-100",
hidden && "h-0 border-none opacity-0",
)}
>
<SearchIcon className="w-4 h-4 text-muted-foreground" />
<input
type="text"
ref={ref}
className="w-full h-full border-none bg-transparent focus:outline-none text-sm"
value={internalValue}
onKeyDown={(e) => {
if (e.key === "Escape") {
onHide();
}
}}
onChange={(e) => setInternalValue(e.target.value)}
placeholder="Search"
/>
{reloading && <Spinner size="small" />}
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/data-table/url-detector.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { Events } from "@/utils/events";
import { useState } from "react";
const urlRegex = /(https?:\/\/\S+)/g;
const imageRegex = /\.(png|jpe?g|gif|webp|svg|ico)(\?.*)?$/i;
const dataImageRegex = /^data:image\//i;
const knownImageDomains = ["avatars.githubusercontent.com"];
const ImageWithFallback = ({ url }: { url: string }) => {
const [error, setError] = useState(false);
if (error) {
return <URLAnchor url={url} />;
}
return (
<div className="flex max-h-[20px] overflow-hidden">
<img
src={url}
alt="URL preview"
className="object-contain max-h-full max-w-full rounded"
onError={() => setError(true)}
/>
</div>
);
};
export const UrlDetector = ({ text }: { text: string }) => {
if (dataImageRegex.test(text)) {
return <ImageWithFallback url={text} />;
}
const createMarkup = (text: string) => {
const parts = text.split(urlRegex);
return parts.map((part, index) => {
if (urlRegex.test(part)) {
const isImage =
imageRegex.test(part) ||
dataImageRegex.test(part) ||
knownImageDomains.some((domain) => part.includes(domain));
if (isImage) {
return <ImageWithFallback key={index} url={part} />;
}
return <URLAnchor key={index} url={part} />;
}
return part;
});
};
return <>{createMarkup(text)}</>;
};
const URLAnchor = ({ url }: { url: string }) => {
return (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
onClick={Events.stopPropagation()}
className="text-link hover:underline"
>
{url}
</a>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/databricks.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Databricks</title><path d="M.95 14.184L12 20.403l9.919-5.55v2.21L12 22.662l-10.484-5.96-.565.308v.77L12 24l11.05-6.218v-4.317l-.515-.309L12 19.118l-9.867-5.653v-2.21L12 16.805l11.05-6.218V6.32l-.515-.308L12 11.974 2.647 6.681 12 1.388l7.76 4.368.668-.411v-.566L12 0 .95 6.27v.72L12 13.207l9.919-5.55v2.26L12 15.52 1.516 9.56l-.565.308Z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/mysql.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>MySQL</title><path d="M16.405 5.501c-.115 0-.193.014-.274.033v.013h.014c.054.104.146.18.214.273.054.107.1.214.154.32l.014-.015c.094-.066.14-.172.14-.333-.04-.047-.046-.094-.08-.14-.04-.067-.126-.1-.18-.153zM5.77 18.695h-.927a50.854 50.854 0 00-.27-4.41h-.008l-1.41 4.41H2.45l-1.4-4.41h-.01a72.892 72.892 0 00-.195 4.41H0c.055-1.966.192-3.81.41-5.53h1.15l1.335 4.064h.008l1.347-4.064h1.095c.242 2.015.384 3.86.428 5.53zm4.017-4.08c-.378 2.045-.876 3.533-1.492 4.46-.482.716-1.01 1.073-1.583 1.073-.153 0-.34-.046-.566-.138v-.494c.11.017.24.026.386.026.268 0 .483-.075.647-.222.197-.18.295-.382.295-.605 0-.155-.077-.47-.23-.944L6.23 14.615h.91l.727 2.36c.164.536.233.91.205 1.123.4-1.064.678-2.227.835-3.483zm12.325 4.08h-2.63v-5.53h.885v4.85h1.745zm-3.32.135l-1.016-.5c.09-.076.177-.158.255-.25.433-.506.648-1.258.648-2.253 0-1.83-.718-2.746-2.155-2.746-.704 0-1.254.232-1.65.697-.43.508-.646 1.256-.646 2.245 0 .972.19 1.686.574 2.14.35.41.877.615 1.583.615.264 0 .506-.033.725-.098l1.325.772.36-.622zM15.5 17.588c-.225-.36-.337-.94-.337-1.736 0-1.393.424-2.09 1.27-2.09.443 0 .77.167.977.5.224.362.336.936.336 1.723 0 1.404-.424 2.108-1.27 2.108-.445 0-.77-.167-.978-.5zm-1.658-.425c0 .47-.172.856-.516 1.156-.344.3-.803.45-1.384.45-.543 0-1.064-.172-1.573-.515l.237-.476c.438.22.833.328 1.19.328.332 0 .593-.073.783-.22a.754.754 0 00.3-.615c0-.33-.23-.61-.648-.845-.388-.213-1.163-.657-1.163-.657-.422-.307-.632-.636-.632-1.177 0-.45.157-.81.47-1.085.315-.278.72-.415 1.22-.415.512 0 .98.136 1.4.41l-.213.476a2.726 2.726 0 00-1.064-.23c-.283 0-.502.068-.654.206a.685.685 0 00-.248.524c0 .328.234.61.666.85.393.215 1.187.67 1.187.67.433.305.648.63.648 1.168zm9.382-5.852c-.535-.014-.95.04-1.297.188-.1.04-.26.04-.274.167.055.053.063.14.11.214.08.134.218.313.346.407.14.11.28.216.427.31.26.16.555.255.81.416.145.094.293.213.44.313.073.05.12.14.214.172v-.02c-.046-.06-.06-.147-.105-.214-.067-.067-.134-.127-.2-.193a3.223 3.223 0 00-.695-.675c-.214-.146-.682-.35-.77-.595l-.013-.014c.146-.013.32-.066.46-.106.227-.06.435-.047.67-.106.106-.027.213-.06.32-.094v-.06c-.12-.12-.21-.283-.334-.395a8.867 8.867 0 00-1.104-.823c-.21-.134-.476-.22-.697-.334-.08-.04-.214-.06-.26-.127-.12-.146-.19-.34-.275-.514a17.69 17.69 0 01-.547-1.163c-.12-.262-.193-.523-.34-.763-.69-1.137-1.437-1.826-2.586-2.5-.247-.14-.543-.2-.856-.274-.167-.008-.334-.02-.5-.027-.11-.047-.216-.174-.31-.235-.38-.24-1.364-.76-1.644-.072-.18.434.267.862.422 1.082.115.153.26.328.34.5.047.116.06.235.107.356.106.294.207.622.347.897.073.14.153.287.247.413.054.073.146.107.167.227-.094.136-.1.334-.154.5-.24.757-.146 1.693.194 2.25.107.166.362.534.703.393.3-.12.234-.5.32-.835.02-.08.007-.133.048-.187v.015c.094.188.188.367.274.555.206.328.566.668.867.895.16.12.287.328.487.402v-.02h-.015c-.043-.058-.1-.086-.154-.133a3.445 3.445 0 01-.35-.4 8.76 8.76 0 01-.747-1.218c-.11-.21-.202-.436-.29-.643-.04-.08-.04-.2-.107-.24-.1.146-.247.273-.32.453-.127.288-.14.642-.188 1.01-.027.007-.014 0-.027.014-.214-.052-.287-.274-.367-.46-.2-.475-.233-1.238-.06-1.785.047-.14.247-.582.167-.716-.042-.127-.174-.2-.247-.303a2.478 2.478 0 01-.24-.427c-.16-.374-.24-.788-.414-1.162-.08-.173-.22-.354-.334-.513-.127-.18-.267-.307-.368-.52-.033-.073-.08-.194-.027-.274.014-.054.042-.075.094-.09.088-.072.335.022.422.062.247.1.455.194.662.334.094.066.195.193.315.226h.14c.214.047.455.014.655.073.355.114.675.28.962.46a5.953 5.953 0 012.085 2.286c.08.154.115.295.188.455.14.33.313.663.455.982.14.315.275.636.476.897.1.14.502.213.682.286.133.06.34.115.46.188.23.14.454.3.67.454.11.076.443.243.463.378z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/snowflake.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Snowflake</title><path d="M24 3.459c0 .646-.418 1.18-1.141 1.18-.723 0-1.142-.534-1.142-1.18 0-.647.419-1.18 1.142-1.18.723 0 1.141.533 1.141 1.18zm-.228 0c0-.533-.38-.951-.913-.951s-.913.38-.913.95c0 .533.38.952.913.952.57 0 .913-.419.913-.951zm-1.37-.533h.495c.266 0 .456.152.456.38 0 .153-.076.229-.19.305l.19.266v.038h-.266l-.19-.266h-.229v.266h-.266zm.495.228h-.229v.267h.229c.114 0 .152-.038.152-.114.038-.077-.038-.153-.152-.153zM7.602 12.4c.038-.151.076-.304.076-.456 0-.114-.038-.228-.038-.342-.114-.343-.304-.647-.646-.838l-4.87-2.777c-.685-.38-1.56-.152-1.94.533-.381.685-.153 1.56.532 1.94l2.701 1.56-2.701 1.56c-.685.38-.913 1.256-.533 1.94.38.685 1.256.914 1.94.533l4.832-2.777c.343-.267.571-.533.647-.876zm1.332 2.626c-.266-.038-.57.038-.837.19l-4.832 2.777c-.685.38-.913 1.256-.532 1.94.38.686 1.255.914 1.94.533l2.701-1.56v3.12c0 .8.647 1.408 1.446 1.408.799 0 1.407-.647 1.407-1.408v-5.592c0-.761-.57-1.37-1.293-1.408zm4.946-6.088c.266.038.57-.038.837-.19l4.832-2.777c.685-.38.913-1.256.532-1.94-.38-.686-1.255-.914-1.94-.533l-2.701 1.56V1.975c0-.799-.647-1.408-1.446-1.408-.799 0-1.446.609-1.446 1.408V7.53c0 .76.609 1.37 1.332 1.407zM3.265 5.97l4.832 2.777c.266.152.533.19.837.19.723-.038 1.331-.684 1.331-1.407V1.975c0-.799-.646-1.408-1.407-1.408-.799 0-1.446.647-1.446 1.408v3.12l-2.701-1.56c-.685-.38-1.56-.152-1.94.533-.419.646-.19 1.521.494 1.902zm9.093 6.011a.412.412 0 00-.114-.266l-.57-.571a.346.346 0 00-.267-.114.412.412 0 00-.266.114l-.571.57a.411.411 0 00-.114.267c0 .076.038.19.114.267l.57.57a.345.345 0 00.267.114c.076 0 .19-.038.266-.114l.571-.57a.412.412 0 00.114-.267zm1.598.533L11.94 14.53c-.039.038-.153.114-.229.114h-.608a.411.411 0 01-.267-.114L8.82 12.514a.408.408 0 01-.076-.229v-.608c0-.076.038-.19.114-.267l2.016-2.016a.41.41 0 01.267-.114h.608a.41.41 0 01.267.114l2.016 2.016a.347.347 0 01.114.267v.608c-.076.077-.114.19-.19.229zm5.593 5.44l-4.832-2.777c-.266-.152-.57-.19-.837-.152-.723.038-1.332.684-1.332 1.408v5.554c0 .8.647 1.408 1.408 1.408.799 0 1.446-.647 1.446-1.408v-3.12l2.7 1.56c.686.38 1.561.152 1.941-.533.419-.646.19-1.521-.494-1.94zm2.549-7.533l-2.701 1.56 2.7 1.56c.686.38.914 1.256.533 1.94-.38.685-1.255.913-1.94.533l-4.832-2.778a1.644 1.644 0 01-.647-.798c-.037-.153-.076-.305-.076-.457 0-.114.039-.228.039-.342.114-.343.342-.647.646-.837l4.832-2.778c.685-.38 1.56-.152 1.94.533.457.609.19 1.484-.494 1.864"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/duckdb.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>DuckDB</title><path d="M12 0C5.363 0 0 5.363 0 12s5.363 12 12 12 12-5.363 12-12S18.637 0 12 0zM9.502 7.03a4.974 4.974 0 0 1 4.97 4.97 4.974 4.974 0 0 1-4.97 4.97A4.974 4.974 0 0 1 4.532 12a4.974 4.974 0 0 1 4.97-4.97zm6.563 3.183h2.351c.98 0 1.787.782 1.787 1.762s-.807 1.789-1.787 1.789h-2.351v-3.551z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/postgresql.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PostgreSQL</title><path d="M23.5594 14.7228a.5269.5269 0 0 0-.0563-.1191c-.139-.2632-.4768-.3418-1.0074-.2321-1.6533.3411-2.2935.1312-2.5256-.0191 1.342-2.0482 2.445-4.522 3.0411-6.8297.2714-1.0507.7982-3.5237.1222-4.7316a1.5641 1.5641 0 0 0-.1509-.235C21.6931.9086 19.8007.0248 17.5099.0005c-1.4947-.0158-2.7705.3461-3.1161.4794a9.449 9.449 0 0 0-.5159-.0816 8.044 8.044 0 0 0-1.3114-.1278c-1.1822-.0184-2.2038.2642-3.0498.8406-.8573-.3211-4.7888-1.645-7.2219.0788C.9359 2.1526.3086 3.8733.4302 6.3043c.0409.818.5069 3.334 1.2423 5.7436.4598 1.5065.9387 2.7019 1.4334 3.582.553.9942 1.1259 1.5933 1.7143 1.7895.4474.1491 1.1327.1441 1.8581-.7279.8012-.9635 1.5903-1.8258 1.9446-2.2069.4351.2355.9064.3625 1.39.3772a.0569.0569 0 0 0 .0004.0041 11.0312 11.0312 0 0 0-.2472.3054c-.3389.4302-.4094.5197-1.5002.7443-.3102.064-1.1344.2339-1.1464.8115-.0025.1224.0329.2309.0919.3268.2269.4231.9216.6097 1.015.6331 1.3345.3335 2.5044.092 3.3714-.6787-.017 2.231.0775 4.4174.3454 5.0874.2212.5529.7618 1.9045 2.4692 1.9043.2505 0 .5263-.0291.8296-.0941 1.7819-.3821 2.5557-1.1696 2.855-2.9059.1503-.8707.4016-2.8753.5388-4.1012.0169-.0703.0357-.1207.057-.1362.0007-.0005.0697-.0471.4272.0307a.3673.3673 0 0 0 .0443.0068l.2539.0223.0149.001c.8468.0384 1.9114-.1426 2.5312-.4308.6438-.2988 1.8057-1.0323 1.5951-1.6698zM2.371 11.8765c-.7435-2.4358-1.1779-4.8851-1.2123-5.5719-.1086-2.1714.4171-3.6829 1.5623-4.4927 1.8367-1.2986 4.8398-.5408 6.108-.13-.0032.0032-.0066.0061-.0098.0094-2.0238 2.044-1.9758 5.536-1.9708 5.7495-.0002.0823.0066.1989.0162.3593.0348.5873.0996 1.6804-.0735 2.9184-.1609 1.1504.1937 2.2764.9728 3.0892.0806.0841.1648.1631.2518.2374-.3468.3714-1.1004 1.1926-1.9025 2.1576-.5677.6825-.9597.5517-1.0886.5087-.3919-.1307-.813-.5871-1.2381-1.3223-.4796-.839-.9635-2.0317-1.4155-3.5126zm6.0072 5.0871c-.1711-.0428-.3271-.1132-.4322-.1772.0889-.0394.2374-.0902.4833-.1409 1.2833-.2641 1.4815-.4506 1.9143-1.0002.0992-.126.2116-.2687.3673-.4426a.3549.3549 0 0 0 .0737-.1298c.1708-.1513.2724-.1099.4369-.0417.156.0646.3078.26.3695.4752.0291.1016.0619.2945-.0452.4444-.9043 1.2658-2.2216 1.2494-3.1676 1.0128zm2.094-3.988-.0525.141c-.133.3566-.2567.6881-.3334 1.003-.6674-.0021-1.3168-.2872-1.8105-.8024-.6279-.6551-.9131-1.5664-.7825-2.5004.1828-1.3079.1153-2.4468.079-3.0586-.005-.0857-.0095-.1607-.0122-.2199.2957-.2621 1.6659-.9962 2.6429-.7724.4459.1022.7176.4057.8305.928.5846 2.7038.0774 3.8307-.3302 4.7363-.084.1866-.1633.3629-.2311.5454zm7.3637 4.5725c-.0169.1768-.0358.376-.0618.5959l-.146.4383a.3547.3547 0 0 0-.0182.1077c-.0059.4747-.054.6489-.115.8693-.0634.2292-.1353.4891-.1794 1.0575-.11 1.4143-.8782 2.2267-2.4172 2.5565-1.5155.3251-1.7843-.4968-2.0212-1.2217a6.5824 6.5824 0 0 0-.0769-.2266c-.2154-.5858-.1911-1.4119-.1574-2.5551.0165-.5612-.0249-1.9013-.3302-2.6462.0044-.2932.0106-.5909.019-.8918a.3529.3529 0 0 0-.0153-.1126 1.4927 1.4927 0 0 0-.0439-.208c-.1226-.4283-.4213-.7866-.7797-.9351-.1424-.059-.4038-.1672-.7178-.0869.067-.276.1831-.5875.309-.9249l.0529-.142c.0595-.16.134-.3257.213-.5012.4265-.9476 1.0106-2.2453.3766-5.1772-.2374-1.0981-1.0304-1.6343-2.2324-1.5098-.7207.0746-1.3799.3654-1.7088.5321a5.6716 5.6716 0 0 0-.1958.1041c.0918-1.1064.4386-3.1741 1.7357-4.4823a4.0306 4.0306 0 0 1 .3033-.276.3532.3532 0 0 0 .1447-.0644c.7524-.5706 1.6945-.8506 2.802-.8325.4091.0067.8017.0339 1.1742.081 1.939.3544 3.2439 1.4468 4.0359 2.3827.8143.9623 1.2552 1.9315 1.4312 2.4543-1.3232-.1346-2.2234.1268-2.6797.779-.9926 1.4189.543 4.1729 1.2811 5.4964.1353.2426.2522.4522.2889.5413.2403.5825.5515.9713.7787 1.2552.0696.087.1372.1714.1885.245-.4008.1155-1.1208.3825-1.0552 1.717-.0123.1563-.0423.4469-.0834.8148-.0461.2077-.0702.4603-.0994.7662zm.8905-1.6211c-.0405-.8316.2691-.9185.5967-1.0105a2.8566 2.8566 0 0 0 .135-.0406 1.202 1.202 0 0 0 .1342.103c.5703.3765 1.5823.4213 3.0068.1344-.2016.1769-.5189.3994-.9533.6011-.4098.1903-1.0957.333-1.7473.3636-.7197.0336-1.0859-.0807-1.1721-.151zm.5695-9.2712c-.0059.3508-.0542.6692-.1054 1.0017-.055.3576-.112.7274-.1264 1.1762-.0142.4368.0404.8909.0932 1.3301.1066.887.216 1.8003-.2075 2.7014a3.5272 3.5272 0 0 1-.1876-.3856c-.0527-.1276-.1669-.3326-.3251-.6162-.6156-1.1041-2.0574-3.6896-1.3193-4.7446.3795-.5427 1.3408-.5661 2.1781-.463zm.2284 7.0137a12.3762 12.3762 0 0 0-.0853-.1074l-.0355-.0444c.7262-1.1995.5842-2.3862.4578-3.4385-.0519-.4318-.1009-.8396-.0885-1.2226.0129-.4061.0666-.7543.1185-1.0911.0639-.415.1288-.8443.1109-1.3505.0134-.0531.0188-.1158.0118-.1902-.0457-.4855-.5999-1.938-1.7294-3.253-.6076-.7073-1.4896-1.4972-2.6889-2.0395.5251-.1066 1.2328-.2035 2.0244-.1859 2.0515.0456 3.6746.8135 4.8242 2.2824a.908.908 0 0 1 .0667.1002c.7231 1.3556-.2762 6.2751-2.9867 10.5405zm-8.8166-6.1162c-.025.1794-.3089.4225-.6211.4225a.5821.5821 0 0 1-.0809-.0056c-.1873-.026-.3765-.144-.5059-.3156-.0458-.0605-.1203-.178-.1055-.2844.0055-.0401.0261-.0985.0925-.1488.1182-.0894.3518-.1226.6096-.0867.3163.0441.6426.1938.6113.4186zm7.9305-.4114c.0111.0792-.049.201-.1531.3102-.0683.0717-.212.1961-.4079.2232a.5456.5456 0 0 1-.075.0052c-.2935 0-.5414-.2344-.5607-.3717-.024-.1765.2641-.3106.5611-.352.297-.0414.6111.0088.6356.1851z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/sqlalchemy.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>SQLAlchemy</title><path d="M11.8 15.955a44.068 44.068 0 0 1-1.673-.691c-1.736-.757-1.981-.772-2.499-.143-.119.146-.25.236-.287.2-.111-.111.219-.644.617-.993.325-.285.433-.325.791-.285.228.025.985.29 1.682.586 1.573.669 2.034.811 2.635.811.731 0 1.106-.512.876-1.192-.057-.171-.04-.228.074-.228.213 0 .322.797.168 1.255a1.617 1.617 0 0 1-.424.614c-.251.211-.41.257-.879.254a3.853 3.853 0 0 1-1.082-.188h.001Zm.301-2.225c0-.048.179-.134.401-.188l.401-.099.086-1.446c.094-1.599.025-3.172-.148-3.383-.063-.074-.253-.165-.427-.205-.705-.156-.236-.264 1.133-.264 1.368 0 1.803.099 1.152.264-.561.14-.564.148-.564 2.43 0 1.266.046 2.22.111 2.342.092.171.228.207.752.207 1.081 0 1.453-.255 1.747-1.203.088-.284.315-.233.236.054-.037.134-.097.54-.134.91l-.068.669H14.44c-1.286 0-2.339-.04-2.339-.088Zm5.312-.068c0-.086.083-.171.219-.236.183-.086.302-.265.734-1.11.686-1.337 1.767-3.634 1.87-3.978.079-.262.097-.276.392-.31.171-.02.313-.031.316-.025l.527 1.152c.284.628.856 1.824 1.271 2.654.695 1.397.772 1.523 1.005 1.636.142.069.253.174.253.237 0 .099-.122.111-1.175.111-1.056 0-1.175-.012-1.175-.114 0-.068.091-.142.236-.191.134-.043.236-.122.236-.182 0-.057-.139-.432-.31-.834l-.31-.731h-2.35l-.225.495c-.421.928-.43 1.147-.037 1.252.196.054.25.097.227.185-.025.103-.127.117-.867.117-.794.006-.837 0-.837-.128Zm-15.652.025a10.933 10.933 0 0 1-.808-.196l-.549-.154.282-.518.281-.518-.227-.281c-.322-.399-.737-1.272-.74-1.554-.003-.657.851-1.61 1.898-2.122.72-.353 1.291-.362 2.009-.026l.54.253.157-.224c.085-.123.156-.285.156-.356 0-.071.071-.134.157-.134.085 0 .156.023.156.048 0 .063-.629 1.651-.669 1.691-.017.016-.187-.063-.381-.177-.546-.321-1.232-.535-1.764-.549-1.238-.031-1.667 1.178-.794 2.236l.308.373.839-.68c.942-.76 1.05-.777 1.784-.27.825.569.839 1.434.042 2.339-.705.805-1.431 1.027-2.677.819Zm5.984-.165c-.646-.301-1.229-.876-1.565-1.547-.538-1.076-.373-1.765.646-2.695.856-.782 1.556-1.087 2.498-1.087.68 0 .825.037 1.266.307 1.044.646 1.303 1.878.675 3.221-.737 1.577-2.294 2.37-3.52 1.801Zm-3.872-.702c.409-.322.381-.917-.063-1.389-.558-.592-.731-.566-1.713.253-.976.814-.982.783.185 1.155.771.251 1.255.242 1.591-.019Zm6.034-.046c.484-.239.817-1.343.68-2.259-.17-1.13-1.698-1.901-2.819-1.423-1.153.493-1.17 1.804-.037 2.985.791.828 1.471 1.044 2.176.697Zm11.359-1.414c.04-.071-.845-2.003-.928-2.023-.06-.017-.976 1.872-.976 2.014 0 .072 1.861.08 1.904.009Z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/sqlite.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>SQLite</title><path d="M21.678.521c-1.032-.92-2.28-.55-3.513.544a8.71 8.71 0 0 0-.547.535c-2.109 2.237-4.066 6.38-4.674 9.544.237.48.422 1.093.544 1.561a13.044 13.044 0 0 1 .164.703s-.019-.071-.096-.296l-.05-.146a1.689 1.689 0 0 0-.033-.08c-.138-.32-.518-.995-.686-1.289-.143.423-.27.818-.376 1.176.484.884.778 2.4.778 2.4s-.025-.099-.147-.442c-.107-.303-.644-1.244-.772-1.464-.217.804-.304 1.346-.226 1.478.152.256.296.698.422 1.186.286 1.1.485 2.44.485 2.44l.017.224a22.41 22.41 0 0 0 .056 2.748c.095 1.146.273 2.13.5 2.657l.155-.084c-.334-1.038-.47-2.399-.41-3.967.09-2.398.642-5.29 1.661-8.304 1.723-4.55 4.113-8.201 6.3-9.945-1.993 1.8-4.692 7.63-5.5 9.788-.904 2.416-1.545 4.684-1.931 6.857.666-2.037 2.821-2.912 2.821-2.912s1.057-1.304 2.292-3.166c-.74.169-1.955.458-2.362.629-.6.251-.762.337-.762.337s1.945-1.184 3.613-1.72C21.695 7.9 24.195 2.767 21.678.521m-18.573.543A1.842 1.842 0 0 0 1.27 2.9v16.608a1.84 1.84 0 0 0 1.835 1.834h9.418a22.953 22.953 0 0 1-.052-2.707c-.006-.062-.011-.141-.016-.2a27.01 27.01 0 0 0-.473-2.378c-.121-.47-.275-.898-.369-1.057-.116-.197-.098-.31-.097-.432 0-.12.015-.245.037-.386a9.98 9.98 0 0 1 .234-1.045l.217-.028c-.017-.035-.014-.065-.031-.097l-.041-.381a32.8 32.8 0 0 1 .382-1.194l.2-.019c-.008-.016-.01-.038-.018-.053l-.043-.316c.63-3.28 2.587-7.443 4.8-9.791.066-.069.133-.128.198-.194Z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icon.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import type { FC } from "react";
import SQLiteIcon from "./icons/sqlite.svg";
import DuckDBIcon from "./icons/duckdb.svg";
import PostgresQLIcon from "./icons/postgresql.svg";
import MySQLIcon from "./icons/mysql.svg";
import SnowflakeIcon from "./icons/snowflake.svg";
import DatabricksIcon from "./icons/databricks.svg";
import ClickhouseIcon from "./icons/clickhouse.svg";
import GoogleBigQueryIcon from "./icons/googlebigquery.svg";
import { cn } from "@/utils/cn";
/**
* Icons are from https://simpleicons.org/
*/
interface DatabaseLogoProps {
name: string;
className?: string;
}
export const DatabaseLogo: FC<DatabaseLogoProps> = ({ name, className }) => {
const lowerName = name.toLowerCase();
const URLS: Record<string, string | undefined> = {
sqlite: SQLiteIcon,
duckdb: DuckDBIcon,
postgres: PostgresQLIcon,
postgresql: PostgresQLIcon,
mysql: MySQLIcon,
snowflake: SnowflakeIcon,
databricks: DatabricksIcon,
clickhouse: ClickhouseIcon,
googlebigquery: GoogleBigQueryIcon,
};
const url = URLS[lowerName];
if (!url) {
return null;
}
return (
<img
src={url}
alt={name}
className={cn("invert-[.5] dark:invert-[.7]", className)}
/>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/engine-variable.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { getCellEditorView } from "@/core/cells/cells";
import type { CellId } from "@/core/cells/ids";
import { goToVariableDefinition } from "@/core/codemirror/go-to-definition/commands";
import { store } from "@/core/state/jotai";
import { variablesAtom } from "@/core/variables/state";
import type { VariableName } from "@/core/variables/types";
import { cn } from "@/utils/cn";
interface Props {
variableName: VariableName;
className?: string;
}
export const EngineVariable: React.FC<Props> = ({
variableName,
className,
}) => {
const onClick = () => {
const cellId = findCellId(variableName);
if (!cellId) {
return;
}
const editorView = getCellEditorView(cellId);
if (editorView) {
goToVariableDefinition(editorView, variableName);
}
};
return (
<button
type="button"
onClick={onClick}
className={cn(
"text-link opacity-80 hover:opacity-100 hover:underline",
className,
)}
>
{variableName}
</button>
);
};
function findCellId(variableName: VariableName): CellId | undefined {
const variables = store.get(variablesAtom);
const variable = variables[variableName];
if (!variable) {
return undefined;
}
return variable.declaredBy[0];
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/display.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
export function dbDisplayName(name: string) {
switch (name) {
case "duckdb":
return "DuckDB";
case "sqlite":
return "SQLite";
case "postgres":
case "postgresql":
return "PostgreSQL";
case "mysql":
return "MySQL";
case "mariadb":
return "MariaDB";
case "mssql":
return "Microsoft SQL Server";
case "oracle":
return "Oracle";
case "redshift":
return "Amazon Redshift";
case "snowflake":
return "Snowflake";
case "bigquery":
return "Google BigQuery";
case "clickhouse":
return "ClickHouse";
case "databricks":
return "Databricks";
case "db2":
return "IBM Db2";
case "hive":
return "Apache Hive";
case "impala":
return "Apache Impala";
case "presto":
return "Presto";
case "trino":
return "Trino";
case "cockroachdb":
return "CockroachDB";
case "timescaledb":
return "TimescaleDB";
case "singlestore":
return "SingleStore";
case "cassandra":
return "Apache Cassandra";
case "mongodb":
return "MongoDB";
default:
return name;
}
}
export function transformDisplayName(displayName: string): string {
const [dbName, engineName] = displayName.split(" ");
if (!engineName) {
return dbDisplayName(displayName);
}
return `${dbDisplayName(dbName)} ${engineName}`;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/datasets/icons.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import type { DataType } from "@/core/kernel/messages";
import {
ToggleLeftIcon,
CalendarIcon,
HashIcon,
TypeIcon,
type LucideIcon,
CalendarClockIcon,
ClockIcon,
CurlyBracesIcon,
} from "lucide-react";
/**
* Maps a data type to an icon.
*/
export const DATA_TYPE_ICON: Record<DataType, LucideIcon> = {
boolean: ToggleLeftIcon,
date: CalendarIcon,
time: ClockIcon,
datetime: CalendarClockIcon,
number: HashIcon,
string: TypeIcon,
integer: HashIcon,
unknown: CurlyBracesIcon,
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/debug/indicator.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
export const TailwindIndicator = () => {
if (process.env.NODE_ENV === "production") {
return null;
}
return (
<div className="fixed bottom-2 right-[300px] z-50 flex size-5 items-center justify-center rounded-lg bg-gray-800 py-3 px-4 font-mono text-xs text-white">
<div className="block sm:hidden">xs</div>
<div className="hidden sm:block md:hidden lg:hidden xl:hidden 2xl:hidden">
sm
</div>
<div className="hidden md:block lg:hidden xl:hidden 2xl:hidden">md</div>
<div className="hidden lg:block xl:hidden 2xl:hidden">lg</div>
<div className="hidden xl:block 2xl:hidden">xl</div>
<div className="hidden 2xl:block">2xl</div>
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/debugger/debugger-code.css
```css
.debugger-input .cm-gutterElement.cm-activeLineGutter {
background-color: #bdc8d2;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/databases/icons/clickhouse.svg
```svg
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>ClickHouse</title><path d="M21.333 10H24v4h-2.667ZM16 1.335h2.667v21.33H16Zm-5.333 0h2.666v21.33h-2.666ZM0 22.665V1.335h2.667v21.33zm5.333-21.33H8v21.33H5.333Z"/></svg>
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/utils/layout.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { graphlib, layout } from "@dagrejs/dagre";
import type { Edge, Node } from "reactflow";
import type { LayoutDirection } from "../types";
const g = new graphlib.Graph().setDefaultEdgeLabel(() => ({}));
export const layoutElements = (
nodes: Node[],
edges: Edge[],
options: { direction: LayoutDirection },
) => {
g.setGraph({
rankdir: options.direction,
nodesep: 150,
ranksep: 200,
// So far, longest-path seems to give the best results as trees are
// generally quite wide.
ranker: "longest-path",
});
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
nodes.forEach((node) =>
g.setNode(node.id, {
...node,
width: node.width ?? 0,
height: node.height ?? 0,
}),
);
layout(g);
return {
nodes: nodes.map((node) => {
const { x, y } = g.node(node.id);
return { ...node, position: { x, y } };
}),
edges,
};
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/utils/changes.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import type {
Node,
NodeAddChange,
NodeRemoveChange,
Edge,
EdgeAddChange,
EdgeRemoveChange,
} from "reactflow";
export function getNodeChanges(
prevNodes: Node[],
nextNodes: Node[],
): Array<NodeAddChange | NodeRemoveChange> {
const changes: Array<NodeAddChange | NodeRemoveChange> = [];
const prevNodeIds = new Set(prevNodes.map((node) => node.id));
const nextNodeIds = new Set(nextNodes.map((node) => node.id));
for (const node of prevNodes) {
if (!nextNodeIds.has(node.id)) {
changes.push({ type: "remove", id: node.id });
}
}
for (const node of nextNodes) {
if (!prevNodeIds.has(node.id)) {
changes.push({ type: "add", item: node });
}
}
return changes;
}
export function getEdgeChanges(
prevEdges: Edge[],
nextEdges: Edge[],
): Array<EdgeAddChange | EdgeRemoveChange> {
const changes: Array<EdgeAddChange | EdgeRemoveChange> = [];
const prevEdgeIds = new Set(prevEdges.map((edge) => edge.id));
const nextEdgeIds = new Set(nextEdges.map((edge) => edge.id));
for (const edge of prevEdges) {
if (!nextEdgeIds.has(edge.id)) {
changes.push({ type: "remove", id: edge.id });
}
}
for (const edge of nextEdges) {
if (!prevEdgeIds.has(edge.id)) {
changes.push({ type: "add", item: edge });
}
}
return changes;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/debugger/debugger-code.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { langs } from "@uiw/codemirror-extensions-langs";
import ReactCodeMirror, {
EditorView,
Prec,
type ReactCodeMirrorRef,
keymap,
} from "@uiw/react-codemirror";
import React, { memo } from "react";
import { Tooltip } from "../ui/tooltip";
import {
HelpCircleIcon,
LayersIcon,
PlayIcon,
SkipForwardIcon,
} from "lucide-react";
import { Button } from "../ui/button";
import { cn } from "@/utils/cn";
import "./debugger-code.css";
import { useKeydownOnElement } from "@/hooks/useHotkey";
import { Functions } from "@/utils/functions";
interface Props {
code: string;
onSubmit: (code: string) => void;
}
export const Debugger: React.FC<Props> = ({ code, onSubmit }) => {
return (
<div className="flex flex-col w-full rounded-b overflow-hidden">
<DebuggerOutput code={code} />
<DebuggerInput onSubmit={onSubmit} />
<DebuggerControls onSubmit={onSubmit} />
</div>
);
};
const DebuggerOutput: React.FC<{
code: string;
}> = memo((props) => {
const ref = React.useRef<ReactCodeMirrorRef>({});
return (
<ReactCodeMirror
minHeight="200px"
maxHeight="200px"
ref={ref}
theme="dark"
value={props.code}
className={"[&>*]:outline-none [&>.cm-editor]:pr-0 overflow-hidden dark"}
readOnly={true}
editable={false}
basicSetup={{
lineNumbers: false,
}}
extensions={[
langs.shell(),
EditorView.updateListener.of((update) => {
if (update.docChanged) {
ref.current.view?.dispatch({
selection: {
anchor: update.state.doc.length,
head: update.state.doc.length,
},
scrollIntoView: true,
});
}
}),
]}
/>
);
});
DebuggerOutput.displayName = "DebuggerOutput";
const DebuggerInput: React.FC<{
onSubmit: (code: string) => void;
}> = ({ onSubmit }) => {
const [value, setValue] = React.useState("");
const ref = React.useRef<HTMLDivElement>(null);
// Capture some events to prevent default behavior
useKeydownOnElement(ref, {
ArrowUp: Functions.NOOP,
ArrowDown: Functions.NOOP,
});
return (
<div ref={ref}>
<ReactCodeMirror
minHeight="18px"
theme="dark"
className={
"debugger-input [&>*]:outline-none cm-focused [&>.cm-editor]:pr-0 overflow-hidden dark border-t-4"
}
value={value}
autoFocus={true}
basicSetup={{
lineNumbers: false,
}}
extensions={[
langs.python(),
Prec.highest(
keymap.of([
{
key: "Enter",
preventDefault: true,
stopPropagation: true,
run: (view: EditorView) => {
const v = value.trim().replaceAll("\n", "\\n");
if (!v) {
return true;
}
onSubmit(v);
setValue("");
return true;
},
},
{
key: "Shift-Enter",
preventDefault: true,
stopPropagation: true,
run: (view: EditorView) => {
// Insert newline and move cursor to end of line
view.dispatch({
changes: {
from: view.state.selection.main.to,
insert: "\n",
},
});
return true;
},
},
]),
),
]}
onChange={(value) => setValue(value)}
/>
</div>
);
};
const DebuggerControls: React.FC<{
onSubmit: (code: string) => void;
}> = ({ onSubmit }) => {
const buttonClasses =
"border m-0 w-9 h-7 bg-[var(--blue-2)] text-[var(--slate-11)] hover:text-[var(--blue-11)] rounded-none border-[var(--blue-2)] hover:bg-[var(--sky-3)] hover:border-[var(--blue-8)]";
const iconClasses = "w-5 h-5";
return (
<div className="flex">
<Tooltip content="Next line">
<Button
variant="text"
size="icon"
data-testid="debugger-next-button"
className={cn(buttonClasses, "rounded-bl-lg")}
onClick={() => onSubmit("n")}
>
<SkipForwardIcon fontSize={36} className={iconClasses} />
</Button>
</Tooltip>
<Tooltip content="Continue execution">
<Button
variant="text"
size="icon"
data-testid="debugger-continue-button"
onClick={() => onSubmit("c")}
className={cn(
buttonClasses,
"text-[var(--grass-11)] hover:text-[var(--grass-11)] hover:bg-[var(--grass-3)] hover:border-[var(--grass-8)]",
)}
>
<PlayIcon fontSize={36} className={iconClasses} />
</Button>
</Tooltip>
<Tooltip content="Print stack trace">
<Button
variant="text"
size="icon"
data-testid="debugger-stack-button"
className={buttonClasses}
onClick={() => onSubmit("bt")}
>
<LayersIcon fontSize={36} className={iconClasses} />
</Button>
</Tooltip>
<Tooltip content="Help">
<Button
variant="text"
size="icon"
data-testid="debugger-help-button"
className={buttonClasses}
onClick={() => onSubmit("help")}
>
<HelpCircleIcon fontSize={36} className={iconClasses} />
</Button>
</Tooltip>
</div>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/utils/useFitToViewOnDimensionChange.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import { useDebouncedCallback } from "@/hooks/useDebounce";
import { useEffect } from "react";
import { useReactFlow, useStore } from "reactflow";
/**
* Call fitToView whenever the dimensions changes
*/
export function useFitToViewOnDimensionChange() {
const instance = useReactFlow();
const width = useStore(({ width }) => width);
const height = useStore(({ height }) => height);
const debounceFitView = useDebouncedCallback(() => {
instance.fitView({ duration: 100 });
}, 100);
// When the window is resized, fit the view to the graph.
useEffect(() => {
if (!width || !height) {
return;
}
debounceFitView();
}, [width, height, debounceFitView]);
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/dependency-graph-minimap.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import ReactFlow, {
type Node,
type Edge,
useEdgesState,
useNodesState,
PanOnScrollMode,
useStore,
type CoordinateExtent,
} from "reactflow";
import React, {
type PropsWithChildren,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { nodeTypes } from "@/components/dependency-graph/custom-node";
import type { Variables } from "@/core/variables/types";
import type { CellId } from "@/core/cells/ids";
import type { CellData } from "@/core/cells/types";
import type { Atom } from "jotai";
import { type NodeData, VerticalElementsBuilder } from "./elements";
import useEvent from "react-use-event-hook";
import { scrollAndHighlightCell } from "../editor/links/cell-link";
import { GraphSelectionPanel } from "./panels";
import { useFitToViewOnDimensionChange } from "./utils/useFitToViewOnDimensionChange";
interface Props {
cellIds: CellId[];
variables: Variables;
cellAtoms: Array<Atom<CellData>>;
}
const elementsBuilder = new VerticalElementsBuilder();
export const DependencyGraphMinimap: React.FC<PropsWithChildren<Props>> = ({
cellIds,
variables,
cellAtoms,
children,
}) => {
// State
const { nodes: initialNodes, edges: allEdges } =
elementsBuilder.createElements(cellIds, cellAtoms, variables, false);
const [edges, setEdges] = useEdgesState([]);
const [nodes, setNodes] = useNodesState(initialNodes);
const [selectedNodeId, setSelectedNodeId] = useState<CellId>();
const [selectedEdge, setSelectedEdge] = useState<Edge>();
const hasRenderer = useRef(false);
// Subscriptions
useFitToViewOnDimensionChange();
const height = useStore(({ height }) => height);
// If the cellIds change, update the nodes.
const syncChanges = useEvent(
(elements: { nodes: Array<Node<NodeData>>; edges: Edge[] }) => {
setNodes(elements.nodes);
setEdges([]);
},
);
// If the cellIds change, update the nodes.
// Only on the second render, because the first render is the initial render.
useEffect(() => {
if (!hasRenderer.current) {
hasRenderer.current = true;
return;
}
syncChanges(
elementsBuilder.createElements(cellIds, cellAtoms, variables, false),
);
}, [cellIds, variables, cellAtoms, syncChanges]);
// If the selected node changes, update the edges.
useEffect(() => {
if (selectedNodeId) {
const selectedEdges = allEdges.filter((edge) => {
const { source, target, data } = edge;
return (
(source === selectedNodeId && data.direction === "outputs") ||
(target === selectedNodeId && data.direction === "inputs")
);
});
setEdges(selectedEdges);
}
}, [selectedNodeId, setEdges, allEdges]);
const translateExtent = useTranslateExtent(nodes, height);
const handleClearSelection = () => {
setSelectedNodeId(undefined);
setSelectedEdge(undefined);
};
const renderGraphSelectionPanel = () => {
if (selectedEdge) {
return (
<GraphSelectionPanel
selection={{
type: "edge",
source: selectedEdge.source as CellId,
target: selectedEdge.target as CellId,
}}
onClearSelection={handleClearSelection}
variables={variables}
edges={edges}
/>
);
}
if (selectedNodeId) {
return (
<GraphSelectionPanel
selection={{ type: "node", id: selectedNodeId }}
variables={variables}
edges={edges}
onClearSelection={handleClearSelection}
/>
);
}
return null;
};
return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
translateExtent={translateExtent}
onNodeClick={(_event, node) => {
const id = node.id;
setSelectedNodeId(id as CellId);
setSelectedEdge(undefined);
setEdges([]);
}}
onNodeDoubleClick={(_event, node) => {
scrollAndHighlightCell(node.id as CellId, "focus");
}}
onEdgeClick={(_event, edge) => {
setSelectedEdge(edge);
}}
// On
snapToGrid={true}
fitView={true}
elementsSelectable={true}
// Off
minZoom={1}
maxZoom={1}
draggable={false}
panOnScrollMode={PanOnScrollMode.Vertical}
zoomOnDoubleClick={false}
nodesDraggable={false}
nodesConnectable={false}
nodesFocusable={false}
edgesFocusable={false}
selectNodesOnDrag={false}
panOnDrag={false}
preventScrolling={false}
zoomOnPinch={false}
panOnScroll={true}
autoPanOnNodeDrag={false}
autoPanOnConnect={false}
>
{renderGraphSelectionPanel()}
{children}
</ReactFlow>
);
};
// Limit the extent of the graph to just the visible nodes.
// The top node and bottom node can be scrolled to the middle of the graph.
function useTranslateExtent(nodes: Node[], height: number): CoordinateExtent {
const PADDING_Y = 10;
return useMemo<CoordinateExtent>(() => {
const top = nodes.reduce(
(top, { position }) => Math.min(top, position.y - height / 2 - PADDING_Y),
Number.POSITIVE_INFINITY,
);
const bottom = nodes.reduce(
(bottom, { position }) =>
Math.max(bottom, position.y + height / 2 + PADDING_Y),
Number.NEGATIVE_INFINITY,
);
return [
[Number.NEGATIVE_INFINITY, top],
[Number.POSITIVE_INFINITY, bottom],
];
}, [nodes, height]);
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/custom-node.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { TinyCode } from "@/components/editor/cell/TinyCode";
import { cn } from "@/utils/cn";
import { useAtomValue } from "jotai";
import React, { memo, useContext } from "react";
import { Handle, Position, useStore } from "reactflow";
import { type CustomNodeProps, getNodeHeight } from "./elements";
import { displayCellName } from "@/core/cells/names";
import { useCellIds } from "@/core/cells/cells";
import type { LayoutDirection } from "./types";
function getWidth(canvasWidth: number) {
const minWidth = 100;
const maxWidth = 400;
const padding = 50;
return Math.min(Math.max(canvasWidth - padding * 2, minWidth), maxWidth);
}
export const EdgeMarkerContext = React.createContext<LayoutDirection>("LR");
const EQUALITY_CHECK = (
prevProps: CustomNodeProps,
nextProps: CustomNodeProps,
) => {
const keys: Array<keyof CustomNodeProps> = ["data", "selected", "id"];
return keys.every((key) => prevProps[key] === nextProps[key]);
};
export const CustomNode = memo((props: CustomNodeProps) => {
const { data, selected } = props; // must match the equality check
const cell = useAtomValue(data.atom);
const cellIndex = useCellIds().inOrderIds.indexOf(cell.id);
const nonSelectedColor = "var(--gray-3)";
const selectedColor = "var(--gray-9)";
const color = selected ? selectedColor : nonSelectedColor;
const reactFlowWidth = useStore(({ width }) => width);
const edgeMarkers = useContext(EdgeMarkerContext);
const linesOfCode = cell.code.split("\n").length;
return (
<div>
<Handle
type="target"
id="inputs"
position={edgeMarkers === "LR" ? Position.Left : Position.Top}
style={{ background: color }}
/>
<Handle
type="source"
id="inputs"
position={edgeMarkers === "LR" ? Position.Left : Position.Top}
style={{ background: color }}
/>
<div
className={cn(
"flex flex-col bg-card border border-input/50 rounded-md mx-[2px] overflow-hidden",
selected && "border-primary",
)}
style={{
height: getNodeHeight(linesOfCode),
width: data.forceWidth || getWidth(reactFlowWidth),
}}
>
<div className="text-muted-foreground font-semibold text-xs py-1 px-2 bg-muted border-b">
{displayCellName(cell.name, cellIndex)}
</div>
<TinyCode code={cell.code} />
</div>
<Handle
type="source"
id="outputs"
position={edgeMarkers === "LR" ? Position.Right : Position.Bottom}
style={{ background: color }}
/>
<Handle
type="target"
id="outputs"
position={edgeMarkers === "LR" ? Position.Right : Position.Bottom}
style={{ background: color }}
/>
</div>
);
}, EQUALITY_CHECK);
CustomNode.displayName = "CustomNode";
export const nodeTypes = {
custom: CustomNode,
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/dependency-graph.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { ReactFlowProvider } from "reactflow";
import React from "react";
import type { Variables } from "@/core/variables/types";
import type { CellId } from "@/core/cells/ids";
import type { CellData } from "@/core/cells/types";
import { type Atom, atom, useAtom } from "jotai";
import type { GraphLayoutView, GraphSettings } from "./types";
import { DependencyGraphMinimap } from "./dependency-graph-minimap";
import { GraphToolbar } from "./panels";
import { DependencyGraphTree } from "./dependency-graph-tree";
import "reactflow/dist/style.css";
import "./dependency-graph.css";
interface Props {
cellIds: CellId[];
variables: Variables;
cellAtoms: Array<Atom<CellData>>;
children?: React.ReactNode;
}
const graphViewAtom = atom<GraphLayoutView>("_minimap_");
const graphViewSettings = atom<GraphSettings>({
hidePureMarkdown: true,
});
export const DependencyGraph: React.FC<Props> = (props) => {
const [layoutDirection, setLayoutDirection] = useAtom(graphViewAtom);
const [settings, setSettings] = useAtom(graphViewSettings);
const renderGraph = () => {
if (layoutDirection === "_minimap_") {
return (
<DependencyGraphMinimap {...props}>
<GraphToolbar
settings={settings}
onSettingsChange={setSettings}
view={layoutDirection}
onChange={setLayoutDirection}
/>
</DependencyGraphMinimap>
);
}
return (
<DependencyGraphTree
{...props}
settings={settings}
layoutDirection={layoutDirection}
>
<GraphToolbar
settings={settings}
onSettingsChange={setSettings}
view={layoutDirection}
onChange={setLayoutDirection}
/>
</DependencyGraphTree>
);
};
return (
<ReactFlowProvider key={layoutDirection}>{renderGraph()}</ReactFlowProvider>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/elements.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import type { CellData } from "@/core/cells/types";
import type { CellId } from "@/core/cells/ids";
import { store } from "@/core/state/jotai";
import type { Variables } from "@/core/variables/types";
import { type Edge, MarkerType, type Node, type NodeProps } from "reactflow";
import type { Atom } from "jotai";
import { Arrays } from "@/utils/arrays";
export interface NodeData {
atom: Atom<CellData>;
forceWidth?: number;
}
export type CustomNodeProps = NodeProps<NodeData>;
export function getNodeHeight(linesOfCode: number) {
const LINE_HEIGHT = 11; // matches TinyCode.css
return Math.min(linesOfCode * LINE_HEIGHT + 35, 200);
}
interface ElementsBuilder {
createElements: (
cellIds: CellId[],
cellAtoms: Array<Atom<CellData>>,
variables: Variables,
hidePureMarkdown: boolean,
) => { nodes: Array<Node<NodeData>>; edges: Edge[] };
}
export class VerticalElementsBuilder implements ElementsBuilder {
private createEdge(source: CellId, target: CellId, direction: string): Edge {
return {
type: "smoothstep",
pathOptions: {
offset: 20,
borderRadius: 100,
},
data: {
direction: direction,
},
markerEnd: {
type: MarkerType.Arrow,
},
id: `${source}-${target}-${direction}`,
source: source,
sourceHandle: direction,
targetHandle: direction,
target: target,
};
}
private createNode(
id: string,
atom: Atom<CellData>,
prevY: number,
): Node<NodeData> {
const linesOfCode = store.get(atom).code.trim().split("\n").length;
const height = getNodeHeight(linesOfCode);
return {
id: id,
data: { atom },
width: 250,
type: "custom",
height: height,
position: { x: 0, y: prevY + 20 },
};
}
createElements(
cellIds: CellId[],
cellAtoms: Array<Atom<CellData>>,
variables: Variables,
hidePureMarkdown: boolean,
) {
let prevY = 0;
const nodes: Array<Node<NodeData>> = [];
const edges: Edge[] = [];
for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
const node = this.createNode(cellId, cellAtom, prevY);
nodes.push(node);
prevY = node.position.y + (node.height || 0);
}
const visited = new Set<string>();
for (const variable of Object.values(variables)) {
const { declaredBy, usedBy } = variable;
for (const fromId of declaredBy) {
for (const toId of usedBy) {
const key = `${fromId}-${toId}`;
if (visited.has(key)) {
continue;
}
visited.add(key);
edges.push(
this.createEdge(fromId, toId, "inputs"),
this.createEdge(fromId, toId, "outputs"),
);
}
}
}
return { nodes, edges };
}
}
export class TreeElementsBuilder implements ElementsBuilder {
private createEdge(source: CellId, target: CellId): Edge {
return {
animated: true,
markerEnd: {
type: MarkerType.ArrowClosed,
},
id: `${source}-${target}`,
// Make thicker
style: { strokeWidth: 2 },
source: source,
sourceHandle: "outputs",
targetHandle: "inputs",
target: target,
};
}
private createNode(id: string, atom: Atom<CellData>): Node<NodeData> {
const linesOfCode = store.get(atom).code.trim().split("\n").length;
const height = getNodeHeight(linesOfCode);
return {
id: id,
data: { atom, forceWidth: 300 },
width: 300,
type: "custom",
height: height,
position: { x: 0, y: 0 },
};
}
createElements(
cellIds: CellId[],
cellAtoms: Array<Atom<CellData>>,
variables: Variables,
hidePureMarkdown: boolean,
) {
const nodes: Array<Node<NodeData>> = [];
const edges: Edge[] = [];
const nodesWithEdges = new Set<CellId>();
const visited = new Set<string>();
for (const variable of Object.values(variables)) {
// Skip marimo, since likely every cell uses it
if (variable.value === "marimo" && variable.name === "mo") {
continue;
}
const { declaredBy, usedBy } = variable;
for (const fromId of declaredBy) {
for (const toId of usedBy) {
const key = `${fromId}-${toId}`;
if (visited.has(key)) {
continue;
}
visited.add(key);
nodesWithEdges.add(fromId);
nodesWithEdges.add(toId);
edges.push(this.createEdge(fromId, toId));
}
}
}
for (const [cellId, cellAtom] of Arrays.zip(cellIds, cellAtoms)) {
// Show every cell
if (!hidePureMarkdown) {
nodes.push(this.createNode(cellId, cellAtom));
}
const hasEdge = nodesWithEdges.has(cellId);
const isMarkdown = store.get(cellAtom).code.trim().startsWith("mo.md");
// Show only cells with edges or non-markdown cells
if (hasEdge || !isMarkdown) {
nodes.push(this.createNode(cellId, cellAtom));
}
}
return { nodes, edges };
}
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/dependency-graph.css
```css
.react-flow__pane.react-flow__pane {
cursor: default;
}
.react-flow__node.react-flow__node {
cursor: pointer;
}
.react-flow__handle.react-flow__handle {
@apply border-background;
}
.react-flow__controls.react-flow__controls {
bottom: 8px;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/dependency-graph-tree.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import ReactFlow, {
useEdgesState,
useNodesState,
Controls,
Background,
BackgroundVariant,
type Node,
type Edge,
ControlButton,
useReactFlow,
} from "reactflow";
import React, { type PropsWithChildren, useEffect, useState } from "react";
import {
EdgeMarkerContext,
nodeTypes,
} from "@/components/dependency-graph/custom-node";
import type { Variables } from "@/core/variables/types";
import type { CellId } from "@/core/cells/ids";
import type { CellData } from "@/core/cells/types";
import type { Atom } from "jotai";
import { type NodeData, TreeElementsBuilder } from "./elements";
import { layoutElements } from "./utils/layout";
import type { GraphSelection, GraphSettings, LayoutDirection } from "./types";
import useEvent from "react-use-event-hook";
import { scrollAndHighlightCell } from "../editor/links/cell-link";
import { GraphSelectionPanel } from "./panels";
import { useFitToViewOnDimensionChange } from "./utils/useFitToViewOnDimensionChange";
import { MapPinIcon } from "lucide-react";
import { store } from "@/core/state/jotai";
import { lastFocusedCellIdAtom } from "@/core/cells/focus";
import { Tooltip } from "../ui/tooltip";
import { Events } from "@/utils/events";
interface Props {
cellIds: CellId[];
variables: Variables;
cellAtoms: Array<Atom<CellData>>;
layoutDirection: LayoutDirection;
settings: GraphSettings;
}
const elementsBuilder = new TreeElementsBuilder();
export const DependencyGraphTree: React.FC<PropsWithChildren<Props>> = ({
cellIds,
variables,
cellAtoms,
children,
layoutDirection,
settings,
}) => {
// eslint-disable-next-line react/hook-use-state
const [initial] = useState(() => {
let elements = elementsBuilder.createElements(
cellIds,
cellAtoms,
variables,
settings.hidePureMarkdown,
);
elements = layoutElements(elements.nodes, elements.edges, {
direction: layoutDirection,
});
return elements;
// Only run once
});
const [nodes, setNodes, onNodesChange] = useNodesState(initial.nodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initial.edges);
const api = useReactFlow();
const syncChanges = useEvent(
(elements: { nodes: Array<Node<NodeData>>; edges: Edge[] }) => {
// Layout the elements
const result = layoutElements(elements.nodes, elements.edges, {
direction: layoutDirection,
});
setNodes(result.nodes);
setEdges(result.edges);
},
);
// If the cellIds change, update the nodes.
useEffect(() => {
syncChanges(
elementsBuilder.createElements(
cellIds,
cellAtoms,
variables,
settings.hidePureMarkdown,
),
);
}, [cellIds, variables, cellAtoms, syncChanges, settings.hidePureMarkdown]);
const [selection, setSelection] = useState<GraphSelection>();
useFitToViewOnDimensionChange();
const handleClearSelection = () => {
setSelection(undefined);
};
return (
<EdgeMarkerContext.Provider value={layoutDirection}>
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
minZoom={0.2}
fitViewOptions={{
minZoom: 0.5,
maxZoom: 1.5,
}}
onNodeClick={(_event, node) => {
setSelection({ type: "node", id: node.id as CellId });
}}
onEdgeClick={(_event, edge) => {
const { source, target } = edge;
setSelection({
type: "edge",
source: source as CellId,
target: target as CellId,
});
}}
onNodeDoubleClick={(_event, node) => {
scrollAndHighlightCell(node.id as CellId, "focus");
}}
fitView={true}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
zoomOnDoubleClick={false}
nodesConnectable={false}
>
<Background color="#ccc" variant={BackgroundVariant.Dots} />
<Controls position="bottom-right" showInteractive={false}>
<Tooltip
content="Jump to focused cell"
delayDuration={200}
side="left"
asChild={false}
>
<ControlButton
onMouseDown={Events.preventFocus}
onClick={() => {
const lastFocusedCell = store.get(lastFocusedCellIdAtom);
// Zoom the graph to the last focused cell
if (lastFocusedCell) {
const node = nodes.find(
(node) => node.id === lastFocusedCell,
);
if (node) {
api.fitView({
padding: 1,
duration: 600,
nodes: [node],
});
setSelection({ type: "node", id: lastFocusedCell });
}
}
}}
>
<MapPinIcon className="size-4" />
</ControlButton>
</Tooltip>
</Controls>
<GraphSelectionPanel
selection={selection}
variables={variables}
edges={edges}
onClearSelection={handleClearSelection}
/>
{children}
</ReactFlow>
</EdgeMarkerContext.Provider>
);
};
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/types.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import type { CellId } from "@/core/cells/ids";
export type LayoutDirection = "TB" | "LR";
export type GraphLayoutView = LayoutDirection | "_minimap_";
export type GraphSelection =
| {
type: "node";
id: CellId;
}
| {
type: "edge";
source: CellId;
target: CellId;
}
| undefined;
export interface GraphSettings {
hidePureMarkdown: boolean;
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/dependency-graph/panels.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import React, { memo } from "react";
import { type Edge, Panel } from "reactflow";
import { Button } from "../ui/button";
import {
Rows3Icon,
NetworkIcon,
ArrowRightFromLineIcon,
ArrowRightIcon,
ArrowRightToLineIcon,
WorkflowIcon,
SquareFunction,
SettingsIcon,
MoreVerticalIcon,
XIcon,
} from "lucide-react";
import type { GraphLayoutView, GraphSelection, GraphSettings } from "./types";
import { CellLink } from "../editor/links/cell-link";
import { CellLinkList } from "../editor/links/cell-link-list";
import { VariableName } from "../variables/common";
import type { Variable, Variables } from "@/core/variables/types";
import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import { Checkbox } from "../ui/checkbox";
import { Label } from "../ui/label";
import { ConnectionCellActionsDropdown } from "../editor/cell/cell-actions";
import { getCellEditorView } from "@/core/cells/cells";
import type { CellId } from "@/core/cells/ids";
import { goToVariableDefinition } from "@/core/codemirror/go-to-definition/commands";
interface Props {
view: GraphLayoutView;
onChange: (view: GraphLayoutView) => void;
settings: GraphSettings;
onSettingsChange: (settings: GraphSettings) => void;
}
export const GraphToolbar: React.FC<Props> = memo(
({ onChange, view, settings, onSettingsChange }) => {
const handleSettingChange = <K extends keyof GraphSettings>(
key: K,
value: GraphSettings[K],
) => {
onSettingsChange({ ...settings, [key]: value });
};
const settingsButton = (
<Popover>
<PopoverTrigger asChild={true}>
<Button variant="text" size="xs">
<SettingsIcon className="w-4 h-4" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-2 text-muted-foreground">
<div className="font-semibold pb-4">Settings</div>
<div className="flex items-center gap-2">
<Checkbox
data-testid="hide-pure-markdown-checkbox"
id="hide-pure-markdown"
checked={settings.hidePureMarkdown}
onCheckedChange={(checked) =>
handleSettingChange("hidePureMarkdown", Boolean(checked))
}
/>
<Label htmlFor="hide-pure-markdown">Hide pure markdown</Label>
</div>
</PopoverContent>
</Popover>
);
return (
<Panel position="top-right" className="flex flex-col items-end gap-2">
<div className="flex gap-2">
<Button
variant="outline"
className="bg-background"
aria-selected={view === "_minimap_"}
size="xs"
onClick={() => onChange("_minimap_")}
>
<Rows3Icon className="w-4 h-4 mr-1" />
Mini Map
</Button>
<Button
variant="outline"
className="bg-background"
aria-selected={view === "TB"}
size="xs"
onClick={() => onChange("TB")}
>
<NetworkIcon className="w-4 h-4 mr-1" />
Vertical Tree
</Button>
<Button
variant="outline"
className="bg-background"
aria-selected={view === "LR"}
size="xs"
onClick={() => onChange("LR")}
>
<NetworkIcon className="w-4 h-4 mr-1 transform -rotate-90" />{" "}
Horizontal Tree
</Button>
</div>
{view !== "_minimap_" && settingsButton}
</Panel>
);
},
);
GraphToolbar.displayName = "GraphToolbar";
export const GraphSelectionPanel: React.FC<{
selection: GraphSelection;
onClearSelection: () => void;
edges: Edge[];
variables: Variables;
}> = memo(({ selection, edges, variables, onClearSelection }) => {
if (!selection) {
return null;
}
// Highlight the variable in the cell editor
const highlightInCell = (cellId: CellId, variableName: string) => {
const editorView = getCellEditorView(cellId);
if (editorView) {
goToVariableDefinition(editorView, variableName);
}
};
const renderSelection = () => {
if (selection.type === "node") {
const variablesUsed = Object.values(variables).filter((variable) =>
variable.usedBy.includes(selection.id),
);
const variablesDeclared = Object.values(variables).filter((variable) =>
variable.declaredBy.includes(selection.id),
);
const renderVariables = (
variables: Variable[],
direction: "in" | "out",
) => (
<>
{variables.length === 0 && (
<div className="text-muted-foreground text-sm text-center">--</div>
)}
<div className="grid grid-cols-5 gap-3 items-center text-sm py-1 flex-1 empty:hidden">
{variables.map((variable) => (
<React.Fragment key={variable.name}>
<VariableName
declaredBy={variable.declaredBy}
name={variable.name}
/>
<div
className="truncate col-span-2"
title={variable.value ?? ""}
>
{variable.value}
<span className="ml-1 truncate text-foreground/60 font-mono">
({variable.dataType})
</span>
</div>
<div className="truncate col-span-2 gap-1 items-center">
<CellLinkList
skipScroll={true}
onClick={() =>
highlightInCell(
direction === "in"
? variable.declaredBy[0]
: variable.usedBy[0],
variable.name,
)
}
maxCount={3}
cellIds={variable.usedBy}
/>
</div>
</React.Fragment>
))}
</div>
</>
);
return (
<>
<div className="font-bold py-2 flex items-center gap-2 border-b px-3">
<SquareFunction className="w-5 h-5" />
<CellLink cellId={selection.id} />
<div className="flex-1" />
<ConnectionCellActionsDropdown cellId={selection.id}>
<Button variant="ghost" size="icon">
<MoreVerticalIcon className="w-4 h-4" />
</Button>
</ConnectionCellActionsDropdown>
<Button
variant="text"
size="icon"
onClick={() => {
onClearSelection();
}}
>
<XIcon className="w-4 h-4" />
</Button>
</div>
<div className="text-sm flex flex-col py-3 pl-2 pr-4 flex-1 justify-center">
<div className="flex flex-col gap-2">
<span className="flex items-center gap-2 font-semibold">
<ArrowRightFromLineIcon className="w-4 h-4" />
Outputs
</span>
{renderVariables(variablesDeclared, "out")}
</div>
<hr className="border-divider my-3" />
<div className="flex flex-col gap-2">
<span className="flex items-center gap-2 font-semibold">
<ArrowRightToLineIcon className="w-4 h-4" />
Inputs
</span>
{renderVariables(variablesUsed, "in")}
</div>
</div>
</>
);
}
if (selection.type === "edge") {
const edgeVariables = Object.values(variables).filter(
(variable) =>
variable.declaredBy.includes(selection.source) &&
variable.usedBy.includes(selection.target),
);
return (
<>
<div className="font-bold py-2 flex items-center gap-2 border-b px-3">
<WorkflowIcon className="w-4 h-4" />
<CellLink cellId={selection.source} />
<ArrowRightIcon className="w-4 h-4" />
<CellLink cellId={selection.target} />
</div>
<div className="grid grid-cols-4 gap-3 max-w-[350px] items-center text-sm p-3 flex-1">
{edgeVariables.map((variable) => (
<React.Fragment key={variable.name}>
<VariableName
declaredBy={variable.declaredBy}
name={variable.name}
onClick={() => {
highlightInCell(variable.declaredBy[0], variable.name);
}}
/>
<div className="truncate text-foreground/60 font-mono">
{variable.dataType}
</div>
<div
className="truncate col-span-2"
title={variable.value ?? ""}
>
{variable.value}
</div>
</React.Fragment>
))}
</div>
</>
);
}
};
return (
<Panel
position="bottom-left"
className="max-h-[90%] flex flex-col w-[calc(100%-5rem)]"
>
<div className="min-h-[100px] shadow-md rounded-md border max-w-[550px] border-primary/40 my-4 min-w-[240px] bg-[var(--slate-1)] text-muted-foreground/80 flex flex-col overflow-y-auto">
{renderSelection()}
</div>
</Panel>
);
});
GraphSelectionPanel.displayName = "GraphSelectionPanel";
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/__tests__/data-attributes.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { render } from "@testing-library/react";
import { beforeAll, describe, expect, it } from "vitest";
import { Cell } from "../Cell";
import { OutputArea } from "../Output";
import type { CellId } from "@/core/cells/ids";
import type { OutputMessage } from "@/core/kernel/messages";
import { Functions } from "@/utils/functions";
import type { UserConfig } from "@/core/config/config-schema";
import { TooltipProvider } from "@/components/ui/tooltip";
import type { AppMode } from "@/core/mode";
beforeAll(() => {
global.ResizeObserver = class ResizeObserver {
observe() {
// do nothing
}
unobserve() {
// do nothing
}
disconnect() {
// do nothing
}
};
global.HTMLDivElement.prototype.scrollIntoView = () => {
// do nothing
};
});
describe("Cell data attributes", () => {
it.each(["edit", "read", "present"])(
"should render cell with data-cell-id and data-cell-name in %s mode",
(mode) => {
const cellId = "test" as CellId;
const cellName = "test_cell";
const userConfig: UserConfig = {
display: {
cell_output: "below",
code_editor_font_size: 14,
dataframes: "rich",
default_width: "normal",
theme: "light",
},
keymap: { preset: "default" },
completion: {
activate_on_typing: true,
copilot: false,
},
formatting: { line_length: 88 },
package_management: { manager: "pip" },
runtime: {
auto_instantiate: false,
auto_reload: "off",
on_cell_change: "lazy",
},
server: {
browser: "default",
follow_symlink: false,
},
save: { autosave: "off", autosave_delay: 1000, format_on_save: false },
ai: {},
};
const { container } = render(
<TooltipProvider>
<Cell
id={cellId}
name={cellName}
code=""
output={null}
consoleOutputs={[]}
status="idle"
edited={false}
interrupted={false}
errored={false}
stopped={false}
staleInputs={false}
runStartTimestamp={null}
lastRunStartTimestamp={null}
runElapsedTimeMs={null}
serializedEditorState={null}
mode={mode as AppMode}
debuggerActive={false}
appClosed={false}
canDelete={true}
updateCellCode={Functions.NOOP}
prepareForRun={Functions.NOOP}
createNewCell={Functions.NOOP}
deleteCell={Functions.NOOP}
focusCell={Functions.NOOP}
moveCell={Functions.NOOP}
setStdinResponse={Functions.NOOP}
moveToNextCell={Functions.NOOP}
updateCellConfig={Functions.NOOP}
clearSerializedEditorState={Functions.NOOP}
sendToBottom={Functions.NOOP}
sendToTop={Functions.NOOP}
collapseCell={Functions.NOOP}
expandCell={Functions.NOOP}
userConfig={userConfig}
outline={null}
isCollapsed={false}
collapseCount={0}
config={{
disabled: false,
hide_code: false,
column: null,
}}
canMoveX={false}
theme="light"
showPlaceholder={false}
allowFocus={true}
/>
</TooltipProvider>,
);
const cellElement = container.querySelector(`[data-cell-id="${cellId}"]`);
expect(cellElement).toBeTruthy();
expect(cellElement?.getAttribute("data-cell-name")).toBe(cellName);
},
);
});
describe("Output data attributes", () => {
it("should render output with data-cell-role", () => {
const cellId = "test" as CellId;
const output: OutputMessage = {
channel: "output",
mimetype: "text/plain",
data: "test output",
timestamp: 0,
};
const { container } = render(
<TooltipProvider>
<OutputArea
output={output}
cellId={cellId}
stale={false}
allowExpand={true}
className="test-output"
/>
</TooltipProvider>,
);
const outputElement = container.querySelector('[data-cell-role="output"]');
expect(outputElement).toBeTruthy();
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/name-cell-input.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { Input } from "@/components/ui/input";
import { Tooltip } from "@/components/ui/tooltip";
import { getCellNames, useCellActions } from "@/core/cells/cells";
import type { CellId } from "@/core/cells/ids";
import {
normalizeName,
getValidName,
isInternalCellName,
} from "@/core/cells/names";
import { useOnMount } from "@/hooks/useLifecycle";
import { cn } from "@/utils/cn";
import { Events } from "@/utils/events";
import React, { useRef, useState } from "react";
interface Props
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> {
value: string;
onChange: (newName: string) => void;
placeholder?: string;
}
export const NameCellInput: React.FC<Props> = ({
value,
onChange,
placeholder,
...props
}) => {
const ref = useRef<HTMLInputElement>(null);
const inputProps = useCellNameInput(value, onChange);
// Custom onBlur without React's synthetic events
// See https://github.com/facebook/react/issues/12363
useOnMount(() => {
const onBlur = inputProps.onBlur;
const input = ref.current;
if (!input) {
return;
}
input.addEventListener("blur", onBlur);
return () => {
input.removeEventListener("blur", onBlur);
};
});
return (
<Input
data-testid="cell-name-input"
value={inputProps.value}
onChange={inputProps.onChange}
ref={ref}
placeholder={placeholder}
className="shadow-none! hover:shadow-none focus:shadow-none focus-visible:shadow-none"
onKeyDown={Events.onEnter(Events.stopPropagation())}
{...props}
/>
);
};
export const NameCellContentEditable: React.FC<{
cellId: CellId;
value: string;
className: string;
}> = ({ value, cellId, className }) => {
const { updateCellName } = useCellActions();
const inputProps = useCellNameInput(value, (newName) =>
updateCellName({ cellId, name: newName }),
);
// If the name is the default, don't render the content editable
if (isInternalCellName(value)) {
return null;
}
return (
<Tooltip content="Click to rename">
<span
className={cn(
"outline-none border hover:border-cyan-500/40 focus:border-cyan-500/40",
className,
)}
contentEditable={true}
suppressContentEditableWarning={true}
onChange={inputProps.onChange}
onBlur={inputProps.onBlur}
onKeyDown={Events.onEnter((e) => {
if (e.target instanceof HTMLElement) {
e.target.blur();
}
})}
>
{value}
</span>
</Tooltip>
);
};
function useCellNameInput(value: string, onChange: (newName: string) => void) {
const [internalValue, setInternalValue] = useState(value);
const commit = (newValue: string) => {
// No change
if (newValue === value) {
return;
}
// Empty
if (!newValue || isInternalCellName(newValue)) {
onChange(newValue);
return;
}
// Get unique name
const validName = getValidName(newValue, getCellNames());
onChange(validName);
};
return {
value: isInternalCellName(internalValue) ? "" : internalValue,
onChange: (evt: React.ChangeEvent<HTMLInputElement>) => {
const newValue = evt.target.value;
const normalized = normalizeName(newValue);
setInternalValue(normalized);
},
onBlur: (evt: Pick<Event, "target">) => {
if (evt.target instanceof HTMLInputElement) {
const newValue = evt.target.value;
commit(normalizeName(newValue));
} else if (evt.target instanceof HTMLSpanElement) {
const newValue = evt.target.innerText.trim();
commit(normalizeName(newValue));
}
},
};
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/types.ts
```ts
/* Copyright 2024 Marimo. All rights reserved. */
import type { HotkeyAction } from "@/core/hotkeys/hotkeys";
/**
* Shared interface to render a user action in the editor.
* This can be in a dropdown menu, context menu, or toolbar.
*/
export interface ActionButton {
label: string;
labelElement?: React.ReactNode;
description?: string;
disabled?: boolean;
tooltip?: React.ReactNode;
variant?: "danger" | "muted" | "disabled";
disableClick?: boolean;
icon?: React.ReactElement;
hidden?: boolean;
rightElement?: React.ReactNode;
hotkey?: HotkeyAction;
handle: (event?: Event) => void;
/**
* Special handler for headless contexts: e.g. a command palette.
*/
handleHeadless?: (event?: Event) => void;
divider?: boolean;
dropdown?: ActionButton[];
}
export function isParentAction(
action: ActionButton,
): action is ActionButton & { dropdown: ActionButton[] } {
return action.dropdown !== undefined;
}
/**
* Flattens all actions into a single array.
* Any parent actions will be removed, but their labels will be prepended to the child actions.
*/
export function flattenActions(
actions: ActionButton[],
prevLabel = "",
): ActionButton[] {
return actions.flatMap((action) => {
// If label is empty, hide
if (!action.label) {
return [];
}
if (isParentAction(action)) {
return flattenActions(action.dropdown, `${prevLabel + action.label} > `);
}
return { ...action, label: prevLabel + action.label };
});
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/useCellActionButton.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { downloadCellOutput } from "@/components/export/export-output-button";
import { Switch } from "@/components/ui/switch";
import { formatEditorViews } from "@/core/codemirror/format";
import { toggleToLanguage } from "@/core/codemirror/language/commands";
import { hasOnlyOneCellAtom, useCellActions } from "@/core/cells/cells";
import {
ImageIcon,
Code2Icon,
ZapIcon,
PlusCircleIcon,
ChevronUpIcon,
ChevronDownIcon,
ChevronsUpIcon,
ChevronsDownIcon,
Trash2Icon,
ZapOffIcon,
PlayIcon,
TextCursorInputIcon,
EyeIcon,
EyeOffIcon,
SparklesIcon,
DatabaseIcon,
Columns2Icon,
XCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
ScissorsIcon,
} from "lucide-react";
import type { ActionButton } from "./types";
import { MultiIcon } from "@/components/icons/multi-icon";
import type { CellData } from "@/core/cells/types";
import type { CellId } from "@/core/cells/ids";
import { saveCellConfig } from "@/core/network/requests";
import type { EditorView } from "@codemirror/view";
import { useRunCell } from "../cell/useRunCells";
import { NameCellInput } from "./name-cell-input";
import { useAtomValue, useSetAtom } from "jotai";
import { aiCompletionCellAtom } from "@/core/ai/state";
import { useImperativeModal } from "@/components/modal/ImperativeModal";
import {
DialogContent,
DialogTitle,
DialogHeader,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import { MarkdownIcon, PythonIcon } from "../cell/code/icons";
import {
aiEnabledAtom,
appWidthAtom,
autoInstantiateAtom,
} from "@/core/config/config";
import { useDeleteCellCallback } from "../cell/useDeleteCell";
import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
import type { CellConfig, RuntimeState } from "@/core/network/types";
import { kioskModeAtom } from "@/core/mode";
import { switchLanguage } from "@/core/codemirror/language/extension";
import { useSplitCellCallback } from "../cell/useSplitCell";
export interface CellActionButtonProps
extends Pick<CellData, "name" | "config"> {
cellId: CellId;
status: RuntimeState;
hasOutput: boolean;
hasConsoleOutput: boolean;
getEditorView: () => EditorView | null;
}
interface Props {
cell: CellActionButtonProps | null;
}
export function useCellActionButtons({ cell }: Props) {
const {
createNewCell: createCell,
updateCellConfig,
updateCellCode,
updateCellName,
moveCell,
sendToTop,
sendToBottom,
addColumnBreakpoint,
clearCellOutput,
} = useCellActions();
const splitCell = useSplitCellCallback();
const runCell = useRunCell(cell?.cellId);
const hasOnlyOneCell = useAtomValue(hasOnlyOneCellAtom);
const canDelete = !hasOnlyOneCell;
const deleteCell = useDeleteCellCallback();
const { openModal } = useImperativeModal();
const setAiCompletionCell = useSetAtom(aiCompletionCellAtom);
const aiEnabled = useAtomValue(aiEnabledAtom);
const autoInstantiate = useAtomValue(autoInstantiateAtom);
const kioskMode = useAtomValue(kioskModeAtom);
const appWidth = useAtomValue(appWidthAtom);
if (!cell || kioskMode) {
return [];
}
const {
cellId,
config,
getEditorView,
name,
hasOutput,
hasConsoleOutput,
status,
} = cell;
const toggleDisabled = async () => {
const newConfig = { disabled: !config.disabled };
await saveCellConfig({ configs: { [cellId]: newConfig } });
updateCellConfig({ cellId, config: newConfig });
};
const toggleHideCode = async () => {
const newConfig: Partial<CellConfig> = { hide_code: !config.hide_code };
await saveCellConfig({ configs: { [cellId]: newConfig } });
updateCellConfig({ cellId, config: newConfig });
const editorView = getEditorView();
// If we're hiding the code, we should blur the editor
// otherwise, we should focus it
if (editorView) {
if (newConfig.hide_code) {
editorView.contentDOM.blur();
} else {
editorView.focus();
}
}
};
// Actions
const actions: ActionButton[][] = [
[
{
icon: <TextCursorInputIcon size={13} strokeWidth={1.5} />,
label: "Name",
disableClick: true,
handle: (evt) => {
evt?.stopPropagation();
evt?.preventDefault();
},
handleHeadless: () => {
openModal(
<DialogContent>
<DialogHeader>
<DialogTitle>Rename cell</DialogTitle>
</DialogHeader>
<div className="flex items-center justify-between">
<Label htmlFor="cell-name">Cell name</Label>
<NameCellInput
placeholder={"cell name"}
value={name}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
openModal(null);
}
}}
onChange={(newName) =>
updateCellName({ cellId, name: newName })
}
/>
</div>
</DialogContent>,
);
},
rightElement: (
<NameCellInput
placeholder={"cell name"}
value={name}
onChange={(newName) => updateCellName({ cellId, name: newName })}
/>
),
},
{
icon: <PlayIcon size={13} strokeWidth={1.5} />,
label: "Run cell",
hotkey: "cell.run",
hidden:
status === "running" ||
status === "queued" ||
status === "disabled-transitively" ||
config.disabled,
handle: () => runCell(),
},
{
icon: <SparklesIcon size={13} strokeWidth={1.5} />,
label: "AI completion",
hidden: !aiEnabled,
handle: () => {
setAiCompletionCell((current) =>
current?.cellId === cellId ? null : { cellId },
);
},
hotkey: "cell.aiCompletion",
},
{
icon: <ScissorsIcon size={13} strokeWidth={1.5} />,
label: "Split cell",
hotkey: "cell.splitCell",
handle: () => splitCell({ cellId }),
},
{
icon: <ImageIcon size={13} strokeWidth={1.5} />,
label: "Export output as PNG",
hidden: !hasOutput,
handle: () => downloadCellOutput(cellId),
},
{
icon: <Code2Icon size={13} strokeWidth={1.5} />,
label: "Format cell",
hotkey: "cell.format",
handle: () => {
const editorView = getEditorView();
if (!editorView) {
return;
}
formatEditorViews({ [cellId]: editorView }, updateCellCode);
},
},
{
icon: config.hide_code ? (
<EyeIcon size={13} strokeWidth={1.5} />
) : (
<EyeOffIcon size={13} strokeWidth={1.5} />
),
label: config.hide_code ? "Show code" : "Hide code",
handle: toggleHideCode,
hotkey: "cell.hideCode",
},
{
icon: config.disabled ? (
<ZapOffIcon size={13} strokeWidth={1.5} />
) : (
<ZapIcon size={13} strokeWidth={1.5} />
),
label: "Reactive execution",
rightElement: (
<Switch
data-testid="cell-disable-switch"
checked={!config.disabled}
size="sm"
onCheckedChange={toggleDisabled}
/>
),
handle: toggleDisabled,
},
{
icon: <XCircleIcon size={13} strokeWidth={1.5} />,
label: "Clear output",
hidden: !(hasOutput || hasConsoleOutput),
handle: () => {
clearCellOutput({ cellId });
},
},
],
// View as
[
{
icon: <MarkdownIcon />,
label: "Convert to Markdown",
hotkey: "cell.viewAsMarkdown",
handle: () => {
const editorView = getEditorView();
if (!editorView) {
return;
}
maybeAddMarimoImport(autoInstantiate, createCell);
switchLanguage(editorView, "markdown", { keepCodeAsIs: true });
},
},
{
icon: <DatabaseIcon size={13} strokeWidth={1.5} />,
label: "Convert to SQL",
handle: () => {
const editorView = getEditorView();
if (!editorView) {
return;
}
maybeAddMarimoImport(autoInstantiate, createCell);
switchLanguage(editorView, "sql", { keepCodeAsIs: true });
},
},
{
icon: <PythonIcon />,
label: "Toggle as Python",
handle: () => {
const editorView = getEditorView();
if (!editorView) {
return;
}
maybeAddMarimoImport(autoInstantiate, createCell);
toggleToLanguage(editorView, "python", { force: true });
},
},
],
// Movement
[
{
icon: (
<MultiIcon>
<PlusCircleIcon size={13} strokeWidth={1.5} />
<ChevronUpIcon size={8} strokeWidth={2} />
</MultiIcon>
),
label: "Create cell above",
hotkey: "cell.createAbove",
handle: () => createCell({ cellId, before: true }),
},
{
icon: (
<MultiIcon>
<PlusCircleIcon size={13} strokeWidth={1.5} />
<ChevronDownIcon size={8} strokeWidth={2} />
</MultiIcon>
),
label: "Create cell below",
hotkey: "cell.createBelow",
handle: () => createCell({ cellId, before: false }),
},
{
icon: <ChevronUpIcon size={13} strokeWidth={1.5} />,
label: "Move cell up",
hotkey: "cell.moveUp",
handle: () => moveCell({ cellId, before: true }),
},
{
icon: <ChevronDownIcon size={13} strokeWidth={1.5} />,
label: "Move cell down",
hotkey: "cell.moveDown",
handle: () => moveCell({ cellId, before: false }),
},
{
icon: <ChevronLeftIcon size={13} strokeWidth={1.5} />,
label: "Move cell left",
hotkey: "cell.moveLeft",
handle: () => moveCell({ cellId, direction: "left" }),
hidden: appWidth !== "columns",
},
{
icon: <ChevronRightIcon size={13} strokeWidth={1.5} />,
label: "Move cell right",
hotkey: "cell.moveRight",
handle: () => moveCell({ cellId, direction: "right" }),
hidden: appWidth !== "columns",
},
{
icon: <ChevronsUpIcon size={13} strokeWidth={1.5} />,
label: "Send to top",
hotkey: "cell.sendToTop",
handle: () => sendToTop({ cellId }),
},
{
icon: <ChevronsDownIcon size={13} strokeWidth={1.5} />,
label: "Send to bottom",
hotkey: "cell.sendToBottom",
handle: () => sendToBottom({ cellId }),
},
{
icon: <Columns2Icon size={13} strokeWidth={1.5} />,
label: "Break into new column",
hotkey: "cell.addColumnBreakpoint",
hidden: appWidth !== "columns",
handle: () => addColumnBreakpoint({ cellId }),
},
],
// Delete
[
{
label: "Delete",
hidden: !canDelete,
variant: "danger",
icon: <Trash2Icon size={13} strokeWidth={1.5} />,
handle: () => {
deleteCell({ cellId });
},
},
],
];
// remove hidden
return actions
.map((group) => group.filter((action) => !action.hidden))
.filter((group) => group.length > 0);
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/useCopyNotebook.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { useImperativeModal } from "@/components/modal/ImperativeModal";
import { toast } from "@/components/ui/use-toast";
import { sendCopy } from "@/core/network/requests";
import { PathBuilder, Paths } from "@/utils/paths";
export function useCopyNotebook(source: string | null) {
const { openPrompt, closeModal } = useImperativeModal();
return () => {
if (!source) {
return null;
}
const pathBuilder = PathBuilder.guessDeliminator(source);
const filename = Paths.basename(source);
openPrompt({
title: "Copy notebook",
description: "Enter a new filename for the notebook copy.",
defaultValue: `_${filename}`,
confirmText: "Copy notebook",
spellCheck: false,
onConfirm: (filename: string) => {
const destination = pathBuilder.join(Paths.dirname(source), filename);
sendCopy({
source: source,
destination: destination,
}).then(() => {
closeModal();
toast({
title: "Notebook copied",
description: "A copy of the notebook has been created.",
});
window.open(`/?file=${destination}`, "_blank");
});
},
});
};
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/__tests__/dynamic-favicon.test.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { render } from "@testing-library/react";
import { DynamicFavicon } from "../dynamic-favicon";
import { useCellErrors } from "@/core/cells/cells";
// Mock useCellErrors hook
vi.mock("@/core/cells/cells", () => ({
useCellErrors: vi.fn(),
}));
describe("DynamicFavicon", () => {
let favicon: HTMLLinkElement;
let mockFetch: ReturnType<typeof vi.fn>;
let mockCreateObjectURL: ReturnType<typeof vi.fn>;
let mockRevokeObjectURL: ReturnType<typeof vi.fn>;
beforeEach(() => {
// Mock favicon element
favicon = document.createElement("link");
favicon.rel = "icon";
favicon.href = "./favicon.ico";
document.head.append(favicon);
// Mock fetch
mockFetch = vi.fn().mockResolvedValue({
blob: () => new Blob(),
});
global.fetch = mockFetch;
// Mock URL methods
mockCreateObjectURL = vi.fn().mockReturnValue("blob:mock-url");
mockRevokeObjectURL = vi.fn();
global.URL.createObjectURL = mockCreateObjectURL;
global.URL.revokeObjectURL = mockRevokeObjectURL;
// Mock document.hasFocus
vi.spyOn(document, "hasFocus").mockReturnValue(true);
// Mock useCellErrors to return no errors by default
(useCellErrors as ReturnType<typeof vi.fn>).mockReturnValue([]);
});
afterEach(() => {
favicon.remove();
vi.clearAllMocks();
vi.useRealTimers();
});
it("should update favicon when running state changes", async () => {
render(<DynamicFavicon isRunning={true} />);
expect(mockFetch).toHaveBeenCalledWith("./circle-play.ico");
});
it("should not reset favicon when not in focus", async () => {
vi.spyOn(document, "hasFocus").mockReturnValue(false);
vi.useFakeTimers();
render(<DynamicFavicon isRunning={false} />);
vi.clearAllMocks();
await vi.advanceTimersByTimeAsync(3000);
expect(mockFetch).not.toHaveBeenCalledWith("./favicon.ico");
});
it("should create favicon link if none exists", () => {
favicon.remove();
render(<DynamicFavicon isRunning={true} />);
const newFavicon = document.querySelector("link[rel~='icon']");
expect(newFavicon).not.toBeNull();
});
it("should cleanup object URLs on unmount", () => {
const { unmount } = render(<DynamicFavicon isRunning={true} />);
unmount();
expect(mockRevokeObjectURL).toHaveBeenCalled();
});
describe("notifications", () => {
beforeEach(() => {
vi.spyOn(document, "visibilityState", "get").mockReturnValue("hidden");
// @ts-expect-error ok in tests
global.Notification = vi.fn();
// @ts-expect-error ok in tests
global.Notification.permission = "granted";
});
it("should send success notification when run completes without errors", () => {
// @ts-expect-error ok in tests
global.Notification = vi.fn().mockImplementation((title, options) => {
expect(title).toBe("Execution completed");
expect(options).toEqual({
body: "Your notebook run completed successfully.",
icon: expect.any(String),
});
});
// @ts-expect-error ok in tests
global.Notification.permission = "granted";
const { rerender } = render(<DynamicFavicon isRunning={true} />);
rerender(<DynamicFavicon isRunning={false} />);
});
it("should not send notification when document is visible", () => {
vi.spyOn(document, "visibilityState", "get").mockReturnValue("visible");
const { rerender } = render(<DynamicFavicon isRunning={true} />);
rerender(<DynamicFavicon isRunning={false} />);
expect(Notification).not.toHaveBeenCalled();
});
it("should request permission if not granted", () => {
// @ts-expect-error ok in tests
global.Notification.permission = "default";
global.Notification.requestPermission = vi
.fn()
.mockResolvedValue("granted");
const { rerender } = render(<DynamicFavicon isRunning={true} />);
rerender(<DynamicFavicon isRunning={false} />);
// eslint-disable-next-line @typescript-eslint/unbound-method
expect(Notification.requestPermission).toHaveBeenCalled();
});
});
});
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/useConfigActions.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { saveAppConfig, saveUserConfig } from "@/core/network/requests";
import type { ActionButton } from "./types";
import type { AppConfig, UserConfig } from "@/core/config/config-schema";
import { getAppWidths } from "@/core/config/widths";
import { useAppConfig, useResolvedMarimoConfig } from "@/core/config/config";
import { useTheme } from "@/theme/useTheme";
export function useConfigActions() {
const { theme } = useTheme();
const [config, setConfig] = useResolvedMarimoConfig();
const [appConfig, setAppConfig] = useAppConfig();
const handleUserConfig = async (values: UserConfig) => {
await saveUserConfig({ config: values }).then(() => {
setConfig(values);
});
};
const handleAppConfig = async (values: AppConfig) => {
await saveAppConfig({ config: values }).then(() => {
setAppConfig(values);
});
};
const actions: ActionButton[] = [
...getAppWidths()
.filter((width) => width !== appConfig.width)
.map((width) => ({
label: `App config > Set width=${width}`,
handle: () => {
handleAppConfig({
...appConfig,
width: width,
});
},
})),
{
label: "Config > Toggle dark mode",
handle: () => {
handleUserConfig({
...config,
display: {
// We don't use the config from the setting since
// we want to resolve 'system' to it's current value.
...config.display,
theme: theme === "dark" ? "light" : "dark",
},
});
},
},
{
label: "Config > Switch keymap to VIM",
hidden: config.keymap.preset === "vim",
handle: () => {
handleUserConfig({
...config,
keymap: {
...config.keymap,
preset: "vim",
},
});
},
},
{
// Adding VIM here to make it easy to search
label: "Config > Switch keymap to default (current: VIM)",
hidden: config.keymap.preset === "default",
handle: () => {
handleUserConfig({
...config,
keymap: {
...config.keymap,
preset: "default",
},
});
},
},
{
label: "Config > Disable GitHub Copilot",
handle: () => {
handleUserConfig({
...config,
completion: {
...config.completion,
copilot: false,
},
});
},
hidden: config.completion.copilot !== "github",
},
{
label: "Config > Enable GitHub Copilot",
handle: () => {
handleUserConfig({
...config,
completion: {
...config.completion,
copilot: "github",
},
});
},
hidden: config.completion.copilot === "github",
},
];
return actions.filter((a) => !a.hidden);
}
```
File: /Users/morganmcguire/ML/marimo/frontend/src/components/editor/actions/useNotebookActions.tsx
```tsx
/* Copyright 2024 Marimo. All rights reserved. */
import { kioskModeAtom, viewStateAtom } from "@/core/mode";
import { downloadBlob, downloadHTMLAsImage } from "@/utils/download";
import { useAtom, useAtomValue, useSetAtom } from "jotai";
import {
ImageIcon,
CommandIcon,
ZapIcon,
ZapOffIcon,
BookMarkedIcon,
FolderDownIcon,
ClipboardCopyIcon,
Share2Icon,
PowerSquareIcon,
GlobeIcon,
LinkIcon,
DownloadIcon,
CodeIcon,
PanelLeftIcon,
CheckIcon,
KeyboardIcon,
Undo2Icon,
FileIcon,
Home,
PresentationIcon,
EditIcon,
LayoutTemplateIcon,
Files,
SettingsIcon,
XCircleIcon,
FilePlus2Icon,
FastForwardIcon,
DatabaseIcon,
} from "lucide-react";
import { commandPaletteAtom } from "../controls/command-palette";
import {
canUndoDeletesAtom,
getNotebook,
hasDisabledCellsAtom,
hasEnabledCellsAtom,
useCellActions,
} from "@/core/cells/cells";
import { disabledCellIds, enabledCellIds } from "@/core/cells/utils";
import {
exportAsMarkdown,
readCode,
saveCellConfig,
} from "@/core/network/requests";
import { Objects } from "@/utils/objects";
import type { ActionButton } from "./types";
import { downloadAsHTML } from "@/core/static/download-html";
import { toast } from "@/components/ui/use-toast";
import { useFilename } from "@/core/saving/filename";
import { useImperativeModal } from "@/components/modal/ImperativeModal";
import { ShareStaticNotebookModal } from "@/components/static-html/share-modal";
import { useRestartKernel } from "./useRestartKernel";
import { createShareableLink } from "@/core/wasm/share";
import { useChromeActions, useChromeState } from "../chrome/state";
import { PANELS } from "../chrome/types";
import { startCase } from "lodash-es";
import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
import { MarkdownIcon } from "@/components/editor/cell/code/icons";
import { Filenames } from "@/utils/filenames";
import { LAYOUT_TYPES } from "../renderers/types";
import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
import { useLayoutState, useLayoutActions } from "@/core/layout/layout";
import { useTogglePresenting } from "@/core/layout/useTogglePresenting";
import { useCopyNotebook } from "./useCopyNotebook";
import { isWasm } from "@/core/wasm/utils";
import { renderShortcut } from "@/components/shortcuts/renderShortcut";
import { copyToClipboard } from "@/utils/copy";
import { newNotebookURL } from "@/utils/urls";
import { useRunAllCells } from "../cell/useRunCells";
import { settingDialogAtom } from "@/components/app-config/state";
import { AddDatabaseDialogContent } from "../database/add-database-form";
const NOOP_HANDLER = (event?: Event) => {
event?.preventDefault();
event?.stopPropagation();
};
export function useNotebookActions() {
const filename = useFilename();
const { openModal, closeModal } = useImperativeModal();
const { openApplication } = useChromeActions();
const { selectedPanel } = useChromeState();
const [viewState] = useAtom(viewStateAtom);
const kioskMode = useAtomValue(kioskModeAtom);
const { updateCellConfig, undoDeleteCell, clearAllCellOutputs } =
useCellActions();
const restartKernel = useRestartKernel();
const runAllCells = useRunAllCells();
const copyNotebook = useCopyNotebook(filename);
const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
const hasDisabledCells = useAtomValue(hasDisabledCellsAtom);
const hasEnabledCells = useAtomValue(hasEnabledCellsAtom);
const canUndoDeletes = useAtomValue(canUndoDeletesAtom);
const { selectedLayout } = useLayoutState();
const { setLayoutView } = useLayoutActions();
const togglePresenting = useTogglePresenting();
const renderCheckboxElement = (checked: boolean) => (
<div className="w-8 flex justify-end">
{checked && <CheckIcon size={14} />}
</div>
);
const actions: ActionButton[] = [
{
icon: <Share2Icon size={14} strokeWidth={1.5} />,
label: "Share",
handle: NOOP_HANDLER,
dropdown: [
{
icon: <GlobeIcon size={14} strokeWidth={1.5} />,
label: "Publish HTML to web",
handle: async () => {
openModal(<ShareStaticNotebookModal onClose={closeModal} />);
},
},
{
icon: <LinkIcon size={14} strokeWidth={1.5} />,
label: "Create WebAssembly link",
handle: async () => {
const code = await readCode();
const url = createShareableLink({ code: code.contents });
await copyToClipboard(url);
toast({
title: "Copied",
description: "Link copied to clipboard.",
});
},
},
],
},
{
icon: <DownloadIcon size={14} strokeWidth={1.5} />,
label: "Download",
handle: NOOP_HANDLER,
dropdown: [
{
icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
label: "Download as HTML",
handle: async () => {
if (!filename) {
toast({
variant: "danger",
title: "Error",
description: "Notebooks must be named to be exported.",
});
return;
}
await downloadAsHTML({ filename, includeCode: true });
},
},
{
icon: <FolderDownIcon size={14} strokeWidth={1.5} />,
label: "Download as HTML (exclude code)",
handle: async () => {
if (!filename) {
toast({
variant: "danger",
title: "Error",
description: "Notebooks must be named to be exported.",
});
return;
}
await downloadAsHTML({ filename, includeCode: false });
},
},
{
icon: (
<MarkdownIcon strokeWidth={1.5} style={{ width: 14, height: 14 }} />
),
label: "Download as Markdown",
handle: async () => {
const md = await exportAsMarkdown({ download: false });
downloadBlob(
new Blob([md], { type: "text/plain" }),
Filenames.toMarkdown(document.title),
);
},
},
{
icon: <CodeIcon size={14} strokeWidth={1.5} />,
label: "Download Python code",
handle: async () => {
const code = await readCode();
downloadBlob(
new Blob([code.contents], { type: "text/plain" }),
Filenames.toPY(document.title),
);
},
},
{
divider: true,
icon: <ImageIcon size={14} strokeWidth={1.5} />,
label: "Download as PNG",
disabled: viewState.mode !== "present",
tooltip:
viewState.mode === "present" ? undefined : (
<span>
Only available in app view. <br />
Toggle with: {renderShortcut("global.hideCode", false)}
</span>
),
handle: async () => {
const app = document.getElementById("App");
if (!app) {
return;
}
await downloadHTMLAsImage(app, document.title);
},
},
{
icon: <FileIcon size={14} strokeWidth={1.5} />,
label: "Download as PDF",
disabled: viewState.mode !== "present",
tooltip:
viewState.mode === "present" ? undefined : (
<span>
Only available in app view. <br />
Toggle with: {renderShortcut("global.hideCode", false)}
</span>
),
handle: async () => {
const beforeprint = new Event("export-beforeprint");
const afterprint = new Event("export-afterprint");
function print() {
window.dispatchEvent(beforeprint);
setTimeout(() => window.print(), 0);
setTimeout(() => window.dispatchEvent(afterprint), 0);
}
print();
},
},
],
},
{
divider: true,
icon: <PanelLeftIcon size={14} strokeWidth={1.5} />,
label: "Helper panel",
handle: NOOP_HANDLER,
dropdown: PANELS.flatMap(({ type, Icon, hidden }) => {
if (hidden) {
return [];
}
return {
label: startCase(type),
rightElement: renderCheckboxElement(selectedPanel === type),
icon: <Icon size={14} strokeWidth={1.5} />,
handle: () => openApplication(type),
};
}),
},
{
icon: <PresentationIcon size={14} strokeWidth={1.5} />,
label: "Present as",
handle: NOOP_HANDLER,
dropdown: [
{
icon:
viewState.mode === "present" ? (
<EditIcon size={14} strokeWidth={1.5} />
) : (
<LayoutTemplateIcon size={14} strokeWidth={1.5} />
),
label: "Toggle app view",
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment