Created
February 11, 2025 15:45
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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( | |
"" | |
), | |
} | |
) | |
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(""), | |
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 | |
::: 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 | |
[](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: | |
[](https://marimo.app/GITHUB_URL) | |
=== "Markdown" | |
Replace `GITHUB_URL` with the URL to a notebook on GitHub. | |
```markdown | |
[](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>— 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&family=Lora&family=PT+Sans:wght@400;700&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&family=Lora&family=PT+Sans:wght@400;700&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'Hello, islands! {\"\ud83c\udfdd\ufe0f\" * slider.value}')"' | |
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'Hello, islands! {\"\ud83c\udfdd\ufe0f\" * slider.value}')"' | |
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 | |
 | |
''' | |
) | |
``` | |
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 | |
[](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&family=Lora&family=PT+Sans:wght@400;700&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='"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 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='"mo.md(\"We can also show the island code!\")"' data-label='null' data-language='"python"' data-placeholder='""' 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='"application/vnd.marimo+error"' data-data='[{"msg": "This cell raised an exception: ModuleNotFoundError('No module named 'matplotlib'')", "exception_type": "ModuleNotFoundError", "raising_cell": null, "type": "exception"}]'></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='"# Also run expensive outputs without performing them in the browser\nimport matplotlib.pyplot as plt\nimport numpy as np\nx = np.linspace(0, 2*np.pi, 100)\ny = np.sin(x)\nplt.plot(x, y)\nplt.gca()"' data-label='null' data-language='"python"' data-placeholder='""' 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='"application/vnd.marimo+error"' data-data='[{"msg": "This cell raised an exception: ModuleNotFoundError('No module named 'idk_package'')", "exception_type": "ModuleNotFoundError", "raising_cell": null, "type": "exception"}]'></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">'''</span> | |
<span class="sd"> # Hello, Markdown!</span> | |
<span class="sd"> Use marimo's "`md`" 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"> '''</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">'$f : \mathbf</span><span class="si">{R}</span><span class="s1"> o \mathbf</span><span class="si">{R}</span><span class="s1">$'</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">'''</span> | |
<span class="sd"> \[</span> | |
<span class="sd"> f: \mathbf{R} o \mathbf{R}</span> | |
<span class="sd"> \]</span> | |
<span class="sd"> '''</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&family=Lora&family=PT+Sans:wght@400;700&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='"\nmo.md(\"We can also show the island code!\")\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='"\n# Also run expensive outputs without performing them in the browser\nimport matplotlib.pyplot as plt\nimport numpy as np\nx = np.linspace(0, 2*np.pi, 100)\ny = np.sin(x)\nplt.plot(x, y)\nplt.gca()\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">'''</span> | |
<span class="sd"> # Hello, Markdown!</span> | |
<span class="sd"> Use marimo's "`md`" 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"> '''</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">'$f : \mathbf</span><span class="si">{R}</span><span class="s1"> o \mathbf</span><span class="si">{R}</span><span class="s1">$'</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">'''</span> | |
<span class="sd"> \[</span> | |
<span class="sd"> f: \mathbf{R} o \mathbf{R}</span> | |
<span class="sd"> \]</span> | |
<span class="sd"> '''</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", | |
"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