Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 28 additions & 3 deletions marimo/_plugins/ui/_impl/tables/narwhals_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -715,9 +715,34 @@ def _sanitize_table_value(self, value: Any) -> Any:

# Handle Pillow images
if DependencyManager.pillow.imported():
from PIL import Image
try:
from PIL import Image

if isinstance(value, Image.Image):
return io_to_data_url(value, "image/png")
if isinstance(value, Image.Image):
return io_to_data_url(value, "image/png")
except Exception:
# Catch any exceptions when converting to data URL
pass

# Handle Matplotlib figures
if DependencyManager.matplotlib.imported():
try:
import matplotlib.figure
from matplotlib.axes import Axes

from marimo._output.formatting import as_html
from marimo._plugins.stateless.flex import vstack

if isinstance(value, matplotlib.figure.Figure):
html = as_html(vstack([str(value), value]))
mimetype, data = html._mime_()

if isinstance(value, Axes):
html = as_html(vstack([str(value), value]))
mimetype, data = html._mime_()
return {"mimetype": mimetype, "data": data}
except Exception:
# Catch any exceptions when converting to HTML
pass

return value
122 changes: 122 additions & 0 deletions marimo/_smoke_tests/pandas/pandas_subplots.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "pandas",
# "matplotlib",
# ]
# ///

import marimo

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


@app.cell
def _():
import marimo as mo
return (mo,)


@app.cell(hide_code=True)
def _(mo):
mo.md(
"""
# Issue #6893: Pandas Subplots Not Displaying

This smoke test reproduces the issue where pandas DataFrame box plots
with `subplots=True` don't render properly in marimo.

**Expected behavior**: Box plots should display as images

**Actual behavior**: Only textual representation appears

The issue occurs because `df.plot.box(subplots=True)` returns a numpy
ndarray of matplotlib Axes objects, which marimo doesn't currently format.
"""
)
return


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

# Load NYC taxi data from stable GitHub URL
taxi_url = (
"https://raw.githubusercontent.com/mwaskom/seaborn-data/master/taxis.csv"
)
df = pd.read_csv(taxi_url)
df
return (df,)


@app.cell(hide_code=True)
def _(mo):
mo.md("""## Test Case 1: Box plot WITHOUT subplots (works correctly)""")
return


@app.cell
def _(df):
# This should work - returns a single Axes object
df[["distance", "total"]].plot.box()
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
"""
## Test Case 2: Box plot WITH subplots (reproduces issue #6893)

This is the exact scenario from the bug report.
"""
)
return


@app.cell
def _(df):
# This reproduces the issue - returns ndarray of Axes
df[["distance", "total"]].plot.box(subplots=True)
return


@app.cell(hide_code=True)
def _(mo):
mo.md("""## Test Case 3: Multiple subplots with custom layout""")
return


@app.cell
def _(df):
# Test with 2x2 layout - also returns ndarray of Axes
df[["distance", "total", "fare", "tip"]].plot.box(
subplots=True, layout=(2, 2), figsize=(10, 8)
)
return


@app.cell(hide_code=True)
def _(mo):
mo.md(
"""
## Test Case 4: 1D array of subplots

Test with a single row of subplots.
"""
)
return


@app.cell
def _(df):
# Returns 1D ndarray of Axes
df[["fare", "tip", "tolls"]].plot.box(subplots=True, layout=(1, 3))
return


if __name__ == "__main__":
app.run()
43 changes: 43 additions & 0 deletions marimo/_smoke_tests/sql/numpy_sql.py
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

don't think this file is needed

Copy link
Contributor Author

@mscolnick mscolnick Oct 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

its just a smoke test, i don't see any harm in having a repro commited to the codebase

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

okay, it just seemed irrelevant to this issue & a normal duckdb error. but okay to include

Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import marimo

__generated_with = "0.17.0"
app = marimo.App(width="medium", sql_output="native")


@app.cell
def _():
import marimo as mo
import pandas as pd
import numpy as np
import sqlglot
import pyarrow
return np, pd


@app.cell
def _(np, pd):
example_df = pd.DataFrame(
[
[np.random.random(size=[2, 2]) for _col in range(2)]
for _row in range(3)
],
columns=["A", "B"],
)
return (example_df,)


@app.cell
def _(example_df):
import duckdb

res = duckdb.sql(
f"""
SELECT COUNT(*) FROM example_df
""",
)
print(res)
return


if __name__ == "__main__":
app.run()
114 changes: 114 additions & 0 deletions tests/_plugins/ui/_impl/tables/test_narwhals.py
Original file line number Diff line number Diff line change
Expand Up @@ -1600,3 +1600,117 @@ def test_calculate_top_k_rows_cache_invalidation(df: Any) -> None:

# Verify the actual results are different
assert result1 != result2


@pytest.mark.skipif(not HAS_DEPS, reason="optional dependencies not installed")
class TestSanitizeTableValue:
"""Tests for the _sanitize_table_value method."""

def setUp(self) -> None:
import polars as pl

self.data = pl.DataFrame({"A": [1, 2, 3]})
self.manager = NarwhalsTableManager.from_dataframe(self.data)

def test_sanitize_none(self) -> None:
"""Test that None values are returned as-is."""
manager = self._get_manager()
assert manager._sanitize_table_value(None) is None

def test_sanitize_primitive_values(self) -> None:
"""Test that primitive values are returned unchanged."""
manager = self._get_manager()
assert manager._sanitize_table_value(42) == 42
assert manager._sanitize_table_value("hello") == "hello"
assert manager._sanitize_table_value(3.14) == 3.14
assert manager._sanitize_table_value(True) is True

@pytest.mark.skipif(
not DependencyManager.pillow.has(),
reason="Pillow not installed",
)
def test_sanitize_pillow_image(self) -> None:
"""Test that Pillow images are converted to data URLs."""
from PIL import Image

manager = self._get_manager()

# Create a simple test image
img = Image.new("RGB", (10, 10), color="red")

result = manager._sanitize_table_value(img)

# Verify it returns a data URL string
assert isinstance(result, str)
assert result.startswith("data:image/png;base64,")

@pytest.mark.skipif(
not DependencyManager.matplotlib.has(),
reason="Matplotlib not installed",
)
def test_sanitize_matplotlib_figure(self) -> None:
"""Test that Matplotlib figures are returned unchanged (no conversion)."""
import matplotlib.pyplot as plt

manager = self._get_manager()

# Create a simple figure
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 2, 3])

result = manager._sanitize_table_value(fig)

# Figure is currently returned unchanged because there's no return statement for figures
# (only for axes)
assert result == fig

plt.close(fig)

@pytest.mark.skipif(
not DependencyManager.matplotlib.has(),
reason="Matplotlib not installed",
)
def test_sanitize_matplotlib_axes(self) -> None:
"""Test that Matplotlib axes are converted to HTML."""
import matplotlib.pyplot as plt

manager = self._get_manager()

# Create a simple axes
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 2, 3])

result = manager._sanitize_table_value(ax)

# Verify it returns a dict with mimetype and data
assert isinstance(result, dict)
assert "mimetype" in result
assert "data" in result

plt.close(fig)

def test_sanitize_unsupported_types(self) -> None:
"""Test that unsupported types are returned unchanged."""
manager = self._get_manager()

# Test various unsupported types
class CustomClass:
pass

obj = CustomClass()
assert manager._sanitize_table_value(obj) == obj

# Test dict
d = {"key": "value"}
assert manager._sanitize_table_value(d) == d

# Test list
lst = [1, 2, 3]
assert manager._sanitize_table_value(lst) == lst

def _get_manager(self) -> NarwhalsTableManager[Any]:
"""Helper method to create a manager."""
import polars as pl

data = pl.DataFrame({"A": [1, 2, 3]})
return NarwhalsTableManager.from_dataframe(data)
Loading