Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 178 additions & 0 deletions examples/ui/spreadsheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# /// script
# requires-python = ">=3.10"
# dependencies = [
# "pandas",
# ]
# ///

import marimo

__generated_with = "0.23.11"
app = marimo.App(width="medium")


@app.cell
def _():
import pandas as pd
import marimo as mo

return mo, pd


@app.cell
def _(mo):
mo.md("""
# Interactive Spreadsheet & Python Integration

This example demonstrates how to call Python code and run functions on your spreadsheet data, and how to push updates from the Python environment back into the spreadsheet.
""")
return


@app.cell
def _(pd):
# Initial dataset representing inventory
initial_df = pd.DataFrame({
"Product": ["Laptop", "Mouse", "Keyboard", "Monitor"],
"Category": ["Electronics", "Accessories", "Accessories", "Electronics"],
"Price": [1000.00, 25.00, 80.00, 300.00],
"Quantity": [5, 20, 15, 8],
})
return (initial_df,)


@app.cell
def _(initial_df, mo):
# Setup state so we can push data back to the spreadsheet from Python
get_data, set_data = mo.state(initial_df)
return get_data, set_data


@app.cell
def _():
# Define custom Python functions that will be registered in the spreadsheet
def add_tax(val):
try:
return round(float(val) * 1.20, 2)
except Exception:
return 0.0

def multiply(x, y):
try:
return round(float(x) * float(y), 2)
except Exception:
return 0.0

return add_tax, multiply


@app.cell
def _(add_tax, get_data, mo, multiply):
# Instantiate the spreadsheet with custom Python functions registered
sheet = mo.ui.spreadsheet(
get_data(),
custom_functions={
"add_tax": add_tax,
"multiply": multiply,
},
label="Inventory Spreadsheet"
)
sheet
return (sheet,)


@app.cell
def _(mo):
mo.md("""
### 🚀 Call Python Functions Directly Inside Spreadsheet Cells!
We registered the Python functions `add_tax` and `multiply` to the spreadsheet. You can type them as formulas in any cell:

* Double-click a cell in a new column and type: **`=add_tax(C2)`** to compute price with 20% tax using Python!
* Type: **`=multiply(C2, D2)`** to compute the subtotal in Python!
""")
return


@app.cell
def _(mo):
mo.md("""
### Pattern 1: Call Python functions on spreadsheet values (Reactive Downstream)
The function below runs in Python on the edited spreadsheet data and adds computed columns (`Subtotal`, `Tax`, and `Total`) in real-time.
""")
return


@app.function
# A Python function that performs calculations on the spreadsheet data
def calculate_totals(df):
processed = df.copy()
# Ensure correct column data types before calculation
try:
processed["Price"] = processed["Price"].astype(float)
processed["Quantity"] = processed["Quantity"].astype(float)
processed["Subtotal"] = processed["Price"] * processed["Quantity"]
processed["Tax"] = processed["Subtotal"] * 0.08
processed["Total"] = processed["Subtotal"] + processed["Tax"]
except Exception:
pass
return processed


@app.cell
def _(mo, sheet):
# Call the python function reactively on sheet.value
df_with_totals = calculate_totals(sheet.value)

# Compute summaries in Python
try:
total_items = int(df_with_totals["Quantity"].sum())
total_value = df_with_totals["Total"].sum()

dashboard = mo.vstack([
mo.hstack([
mo.stat(label="Total Items", value=f"{total_items}"),
mo.stat(label="Total Value (inc. Tax)", value=f"${total_value:,.2f}"),
], justify="start", gap=4),
mo.md("#### Computed DataFrame View:"),
mo.ui.table(df_with_totals, selection=None)
])
except Exception as e:
dashboard = mo.md(f"Error executing calculations: {e}")

dashboard
return


@app.cell
def _(mo):
mo.md("""
### Pattern 2: Push changes from Python to the Spreadsheet
Click the button below to execute a Python function that applies a **10% discount** to all items in the inventory and writes the updated values back into the spreadsheet cells.
""")
return


@app.cell
def _(mo, set_data, sheet):
def apply_discount(btn):
# 1. Get the current spreadsheet data frame
df = sheet.value.copy()
try:
# 2. Modify the prices in Python
df["Price"] = (df["Price"].astype(float) * 0.9).round(2)
# 3. Update the state to write the modified data back into the spreadsheet
set_data(df)
except Exception:
pass

discount_button = mo.ui.button(
label="Apply 10% Discount in Python",
on_click=apply_discount,
kind="warn"
)
discount_button
return


if __name__ == "__main__":
app.run()
2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"@dnd-kit/utilities": "^3.2.2",
"@emotion/cache": "^11.14.0",
"@emotion/react": "^11.14.0",
"@fortune-sheet/formula-parser": "^0.2.13",
"@fortune-sheet/react": "^1.0.4",
"@glideapps/glide-data-grid": "6.0.4-alpha9",
"@hookform/resolvers": "^5.2.2",
"@img-comparison-slider/react": "^8.0.2",
Expand Down
143 changes: 143 additions & 0 deletions frontend/src/plugins/impl/SpreadsheetPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
/* Copyright 2026 Marimo. All rights reserved. */

import fortuneCss from "@fortune-sheet/react/dist/index.css?inline";
import React, { useState } from "react";
import { z } from "zod";
import { inferFieldTypes } from "@/components/data-table/columns";
import { LoadingTable } from "@/components/data-table/loading-table";
import { type FieldTypes, toFieldTypes } from "@/components/data-table/types";
import { Alert, AlertTitle } from "@/components/ui/alert";
import { DelayMount } from "@/components/utils/delay-mount";
import { DATA_TYPES } from "@/core/kernel/messages";
import { useAsyncData } from "@/hooks/useAsyncData";
import { createPlugin } from "../core/builder";
import { rpc } from "../core/rpc";
import type { Setter } from "../types";
import { vegaLoadData } from "./vega/loader";
import { getVegaFieldTypes } from "./vega/utils";

type CsvURL = string;
type TableData<T> = T[] | CsvURL;

type FieldTypesProps = any;

export type PluginFunctions = {
run_custom_function: (req: { name: string; args: any[] }) => Promise<any>;
};

const LazyWorkbook = React.lazy(() => import("./spreadsheet/workbook-wrapper"));

export const SpreadsheetPlugin = createPlugin<Record<string, any>[] | null>(
"marimo-spreadsheet",
{
cssStyles: [fortuneCss],
},
)
.withData(
z.object({
label: z.string().nullable().optional(),
data: z.union([z.string(), z.array(z.object({}).passthrough())]),
fieldTypes: z
.array(
z.tuple([
z.coerce.string(),
z.tuple([z.enum(DATA_TYPES), z.string()]),
]),
)
.nullish(),
customFunctions: z
.array(z.string())
.nullish()
.transform((val) => val ?? []),
}),
)
.withFunctions<PluginFunctions>({
run_custom_function: rpc
.input(z.object({ name: z.string(), args: z.array(z.any()) }))
.output(z.any()),
})
.renderer((props) => {
console.log("SpreadsheetPlugin received data:", props.data);
return (
<LoadingSpreadsheet
data={props.data.data}
fieldTypes={props.data.fieldTypes}
customFunctions={props.data.customFunctions}
run_custom_function={props.functions.run_custom_function}
value={props.value}
onChange={props.setValue}
/>
);
});

interface LoadingSpreadsheetProps {
data: TableData<object>;
fieldTypes: FieldTypesProps | null | undefined;
customFunctions: string[];
run_custom_function: PluginFunctions["run_custom_function"];
value: Record<string, any>[] | null;
onChange: Setter<Record<string, any>[] | null>;
}

const LoadingSpreadsheet = (props: LoadingSpreadsheetProps) => {
const [data, setData] = useState<Record<string, any>[]>([]);
const [columnFields, setColumnFields] = useState<FieldTypes>(new Map());
const [isLoading, setIsLoading] = useState(true);

const { error } = useAsyncData(async () => {
setIsLoading(true);
const withoutExternalTypes = toFieldTypes(props.fieldTypes ?? []);

const localData = Array.isArray(props.data)
? (props.data as Record<string, any>[])
: await vegaLoadData(
props.data,
{
type: "csv",
parse: getVegaFieldTypes(Object.fromEntries(withoutExternalTypes)),
},
{ handleBigIntAndNumberLike: true },
);

setData(localData);
setColumnFields(
toFieldTypes(props.fieldTypes ?? inferFieldTypes(localData)),
);
setIsLoading(false);
}, [props.fieldTypes, props.data]);

if (error) {
return (
<Alert variant="destructive" className="mb-2">
<AlertTitle>Error</AlertTitle>
<div className="text-md">
{error.message || "An unknown error occurred"}
</div>
</Alert>
);
}

if (isLoading) {
return (
<DelayMount milliseconds={200}>
<LoadingTable pageSize={10} />
</DelayMount>
);
}

const initialData =
props.value && props.value.length > 0 ? props.value : data;
const columnNames = [...columnFields.keys()];

return (
<React.Suspense fallback={<LoadingTable pageSize={10} />}>
<LazyWorkbook
initialData={initialData}
columnNames={columnNames}
customFunctions={props.customFunctions}
run_custom_function={props.run_custom_function}
onChange={props.onChange}
/>
</React.Suspense>
);
};
2 changes: 2 additions & 0 deletions frontend/src/plugins/impl/spreadsheet/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* Copyright 2026 Marimo. All rights reserved. */
declare module "@fortune-sheet/formula-parser";
Loading
Loading