Chart builder renders an empty chart for columns whose name contains a `.`, when the data falls back to CSV
Describe the bug
What happens
In a mo.ui.table Visualize tab, a column whose name contains a . draws an empty chart — no marks, the numeric axis collapsed to a single tick — but only when the table also contains a timedelta64 column.
Repro
marimo 0.24.0, pandas 3.0.5, altair 6.2.2, pyarrow 25.0.1
import marimo as mo, pandas as pd
df = pd.DataFrame({"a.b": [1.0, 2.0, 3.0], "n": [1, 2, 3]})
df["d"] = pd.to_timedelta([1, 2, 3], unit="D") # remove this line and it works
mo.ui.table(df)
Open Visualize, set X = a.b and Y = n. The plot area is blank — no marks, no tick labels. Drop the d column and the identical chart renders three bars.
The Python code tab shows the field is escaped: x=alt.X(field='a\\.b', type='quantitative').
Cause
Two workarounds for the same Vega-Lite behaviour (a . in field means nested access) that cancel each other out:
replacePeriodsInColumnNames(vega/loader.ts, #1455) rewrites the CSV header,.→ U+2024escapeFieldName(data-table/charts/chart-spec/utils.ts, #6973) escapes the field,.→\.
Vega unescapes a\.b back to the literal key a.b, which the rewritten data no longer has. Every row reads null and Vega-Lite's invalid-value filter drops all of them.
Whether the two ever meet depends on the transport, and that is decided in _maybe_sanitize_dataframe:
return res.to_native() # success: native frame
except Exception as e:
LOGGER.warning(f"Failed to sanitize narwhals dataframe: {e}")
return data # failure: narwhals wrapper — no to_arrow_ipc
The wrapper has no Arrow export, so _to_marimo_arrow falls back to CSV, which is where the header rewriting lives. Altair's sanitizer refuses timedelta64, so any duration column triggers the fallback.
Suggested fixes
altair_transformer.py: returndata.to_native()on the failure path. Arrow export then succeeds with the timedelta still present (checked locally), which fixes the reported symptom and removes a silent transport downgrade.- Make the loader and the chart spec agree — either have
escapeFieldNameapply the same U+2024 substitution, or stop renaming in the loader now that consumers escape.
(1) alone does not fix the underlying mismatch: a dotted column name still breaks on any genuine CSV fallback.
Will you submit a PR?
- Yes
Environment
{ "marimo": "0.24.0", "editable": false, "location": "~/Developer/marimo-pm/.venv/lib/python3.13/site-packages/marimo", "OS": "Linux", "OS Version": "7.1.13-200.fc44.x86_64", "Processor": "", "Python Version": "3.13.5", "Locale": "en_US", "Binaries": { "Browser": "Mozilla/5.0 (X11; Linux x86_64; rv:155.0) Gecko/20100101 Firefox/155.0", "Node": "v22.23.1", "uv": "0.11.12 (x86_64-unknown-linux-gnu)" }, "Dependencies": { "click": "8.5.0", "docutils": "0.23", "itsdangerous": "2.2.0", "jedi": "0.19.2", "markdown": "3.10.3", "narwhals": "2.25.0", "packaging": "26.3", "psutil": "7.2.2", "pygments": "2.21.0", "pymdown-extensions": "11.0.2", "pyyaml": "6.0.3", "starlette": "1.6.0", "tomlkit": "0.15.1", "typing-extensions": "4.16.0", "uvicorn": "0.52.4", "websockets": "17.1" }, "Optional Dependencies": { "altair": "6.2.2", "anywidget": "0.11.0", "loro": "1.13.2", "pandas": "3.0.5", "polars": "1.44.1", "pyarrow": "25.0.1" }, "Experimental Flags": {} }
Code to reproduce
import marimo as mo, pandas as pd
df = pd.DataFrame({"a.b": [1.0, 2.0, 3.0], "n": [1, 2, 3]})
df["d"] = pd.to_timedelta([1, 2, 3], unit="D") # remove this line and it works
mo.ui.table(df)
Source: marimo-team/marimo