TL;DR
Static image export regressed in Kaleido 1.3.0 for Plotly figures containing scalar non-JSON-native objects that Plotly’s PlotlyJSONEncoder can serialize, such as pandas.Timestamp. The raw figure spec may legitimately contain such objects after fig.to_dict(). Plotly’s JSON encoder accepts them, but Kaleido 1.3.0 serializes the spec with orjson.dumps(...) and a narrow fallback that only handles .tolist(). This breaks fig.to_image() / fig.write_image() for figures that worked with Plotly 5 + Kaleido 0.2.1.
Related: appears that same root cause is responsible for #453
I have been using Plotly+Kaleido for image export quite some time. And after migrating to Plotly 6 recently (skipped earlier minor versions), I noticed that some plots are not getting exported into image format, causing this error:
.../lib/python3.13/site-packages/kaleido/_kaleido_tab/_tab.py", line 151, in _calc_fig
spec_str = orjson.dumps(
~~~~~~~~~~~~^
spec,
^^^^^
default=_orjson_default,
^^^^^^^^^^^^^^^^^^^^^^^^
option=orjson.OPT_SERIALIZE_NUMPY,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
).decode()
^
TypeError: Type is not JSON serializable: Timestamp
Despite json type error being as always not very informative, I figured the only candidate is pandas.Timestamp that are present in the dataframe I feed to Plotly express.
So I went onto a bit of a journey, with some help of LLM (disclaimer: I ran all the code it gave and reviewed what it said).
Here's a code that tries a few different Figures to reveal possible rootcause:
import json
import sys
from importlib.metadata import PackageNotFoundError, version
from typing import Any
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.utils import PlotlyJSONEncoder
JsonPath = tuple[str | int, ...]
def package_version(package_name: str) -> str:
"""Return the installed package version, or '<not installed>'."""
try:
return version(package_name)
except PackageNotFoundError:
return "<not installed>"
def print_versions() -> None:
"""Print the interpreter and relevant package versions."""
versions = {
"python": sys.version.split()[0],
"plotly": package_version("plotly"),
"kaleido": package_version("kaleido"),
"orjson": package_version("orjson"),
"pandas": package_version("pandas"),
}
print("versions:", versions)
def timestamp_paths(obj: Any, path: JsonPath = ()) -> list[JsonPath]:
"""Return paths where pandas Timestamp objects occur inside a nested object."""
if isinstance(obj, pd.Timestamp):
return [path]
if isinstance(obj, dict):
paths: list[JsonPath] = []
for key, value in obj.items():
paths.extend(timestamp_paths(value, (*path, key)))
return paths
if isinstance(obj, list | tuple):
paths = []
for index, value in enumerate(obj):
paths.extend(timestamp_paths(value, (*path, index)))
return paths
return []
def probe_orjson_if_available(spec: dict[str, Any]) -> str:
"""Probe Kaleido-1.3-style orjson serialization if orjson is installed."""
try:
import orjson
except ImportError:
return "SKIP: orjson is not installed"
def default(obj: Any) -> Any:
if hasattr(obj, "tolist"):
return obj.tolist()
raise TypeError(f"Type is not JSON serializable: {type(obj).__name__}")
try:
orjson.dumps(
spec,
default=default,
option=orjson.OPT_SERIALIZE_NUMPY,
)
except Exception as exc:
return f"FAIL: {type(exc).__name__}: {exc}"
return "OK"
def probe_figure(name: str, fig: go.Figure) -> None:
"""Print serialization and image-export diagnostics for one figure."""
spec = fig.to_dict()
print(f"\n{name}")
print("-" * len(name))
paths = timestamp_paths(spec)
print("pd.Timestamp paths:", paths or "<none>")
try:
json.dumps(spec, cls=PlotlyJSONEncoder)
except Exception as exc:
print(f"PlotlyJSONEncoder: FAIL: {type(exc).__name__}: {exc}")
else:
print("PlotlyJSONEncoder: OK")
print(f"Kaleido-1.3-style orjson: {probe_orjson_if_available(spec)}")
try:
fig.to_image(format="svg")
except Exception as exc:
print(f"fig.to_image(svg): FAIL: {type(exc).__name__}: {exc}")
else:
print("fig.to_image(svg): OK")
def main() -> None:
"""Run Plotly/Kaleido Timestamp serialization probes."""
print_versions()
probe_figure(
"scalar Timestamp in trace list",
go.Figure(
go.Scatter(
x=[pd.Timestamp("2024-01-01")],
y=[1],
)
),
)
frame = pd.DataFrame(
{
"time": pd.to_datetime(["2024-01-01", "2024-01-02"]),
"value": [1, 2],
}
)
probe_figure(
"datetime64 DataFrame column via Plotly Express",
px.line(frame, x="time", y="value"),
)
fig = px.line(frame, x="time", y="value")
fig.add_vline(x=pd.Timestamp("2024-01-02"))
probe_figure(
"scalar Timestamp in layout shape",
fig,
)
if __name__ == "__main__":
main()
I have two environments, one still on Plotly 5, and a newer one with Plotly 6 and orjson, below is output of the script from running in them:
versions: {'python': '3.13.2', 'plotly': '5.24.1', 'kaleido': '0.2.1', 'orjson': '<not installed>', 'pandas': '2.2.3'}
scalar Timestamp in trace list
------------------------------
pd.Timestamp paths: [('data', 0, 'x', 0)]
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: SKIP: orjson is not installed
fig.to_image(svg): OK
datetime64 DataFrame column via Plotly Express
----------------------------------------------
pd.Timestamp paths: <none>
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: SKIP: orjson is not installed
fig.to_image(svg): OK
scalar Timestamp in layout shape
--------------------------------
pd.Timestamp paths: [('layout', 'shapes', 0, 'x0'), ('layout', 'shapes', 0, 'x1')]
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: SKIP: orjson is not installed
fig.to_image(svg): OK
and
versions: {'python': '3.13.13', 'plotly': '6.8.0', 'kaleido': '1.3.0', 'orjson': '3.11.9', 'pandas': '2.3.3'}
scalar Timestamp in trace list
------------------------------
pd.Timestamp paths: [('data', 0, 'x', 0)]
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: FAIL: TypeError: Type is not JSON serializable: Timestamp
fig.to_image(svg): FAIL: TypeError: Type is not JSON serializable: Timestamp
datetime64 DataFrame column via Plotly Express
----------------------------------------------
pd.Timestamp paths: <none>
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: OK
fig.to_image(svg): OK
scalar Timestamp in layout shape
--------------------------------
pd.Timestamp paths: [('layout', 'shapes', 0, 'x0'), ('layout', 'shapes', 0, 'x1')]
PlotlyJSONEncoder: OK
Kaleido-1.3-style orjson: FAIL: TypeError: Type is not JSON serializable: Timestamp
fig.to_image(svg): FAIL: TypeError: Type is not JSON serializable: Timestamp
Despite this being result of interplay between Plotly, kaleido and orjson, it seems most reasonable to create issue here.
Yes, I am aware that a the end of day, encountering this can be avoided on user side by tending to DataFrame or cleaning Figure before export.
TL;DR
Related: appears that same root cause is responsible for #453
I have been using Plotly+Kaleido for image export quite some time. And after migrating to Plotly 6 recently (skipped earlier minor versions), I noticed that some plots are not getting exported into image format, causing this error:
Despite json type error being as always not very informative, I figured the only candidate is
pandas.Timestampthat are present in the dataframe I feed to Plotly express.So I went onto a bit of a journey, with some help of LLM (disclaimer: I ran all the code it gave and reviewed what it said).
Here's a code that tries a few different Figures to reveal possible rootcause:
I have two environments, one still on Plotly 5, and a newer one with Plotly 6 and
orjson, below is output of the script from running in them:and
Despite this being result of interplay between Plotly, kaleido and orjson, it seems most reasonable to create issue here.
Yes, I am aware that a the end of day, encountering this can be avoided on user side by tending to DataFrame or cleaning Figure before export.