2025, Nov 22 13:00

Create Plotly heatmaps directly from Polars: tidy columns with go.Heatmap or wide tables via px.imshow—no pandas needed

Build Plotly heatmaps from a Polars DataFrame without pandas: use px.imshow with a wide pivot and explicit y labels, or plot tidy columns via go.Heatmap.

Heatmaps are a natural fit for tidy data, but the plotting step often nudges people into pivoting and converting between libraries. If you already have a tidy/long polars DataFrame and want to draw a heatmap without detouring through pandas, there are two clean options: feed a wide polars table to Plotly Express, or pass the tidy columns directly to Plotly’s graph_objects. Both avoid the pandas round trip.

Problem setup

Suppose you start with a tidy polars DataFrame and build a heatmap by pivoting to wide and converting to pandas before plotting. That works, but it’s more ceremony than needed.

import plotly.express as px
import polars as pl

long_pl = pl.DataFrame(
    {
        "x": [10, 10, 10, 20, 20, 20, 30, 30, 30],
        "y": [3, 4, 5, 3, 4, 5, 3, 4, 5],
        "value": [5, 8, 2, 4, 10, 14, 10, 8, 9],
    }
)
print(long_pl)

wide_pd = (
    long_pl.pivot(index="x", on="y", values="value").to_pandas().set_index("x")
)
print(wide_pd)

chart = px.imshow(wide_pd)
chart.show()

What’s going on under the hood

Plotly treats a pandas index as the y-axis when you hand it a DataFrame. A polars DataFrame doesn’t carry that concept of index, so you need to be explicit about what becomes the y labels. That’s the only real difference here, and it’s easy to address without converting to pandas.

If plotly really were to handle polars data natively, I would expect it can handle tidy dataframes, i.e. no need for pivot.

That expectation holds: you can either hand Plotly Express a wide polars table and specify y explicitly, or hand plotly.graph_objects tidy columns directly and let it render the grid.

Solution: wide polars DataFrame straight into Plotly Express

Keep everything in polars, pivot to wide in memory, then tell Plotly what to use as y. Drop the label column from the Z matrix.

import plotly.express as px

wide_pl = long_pl.pivot(index="x", on="y", values="value")
heat1 = px.imshow(wide_pl.drop("x"), y=wide_pl["x"])  # y labels come from the former index
heat1.show()

This produces the same visual arrangement you’d expect from the pandas version, but without any conversion.

Solution: tidy polars DataFrame with plotly.graph_objects

You can also plot from the tidy columns directly. This mirrors how you’d do it with pandas in tidy form.

import plotly.graph_objects as go

heat2 = go.Figure(
    go.Heatmap(
        x=long_pl["y"],
        y=long_pl["x"],
        z=long_pl["value"],
    )
)
# Align orientation with the wide-table output
heat2.update_layout(yaxis_autorange="reversed")
heat2.show()

Alternative: tidy plotting via Altair

If you prefer Altair, polars exposes a .plot namespace that accepts tidy data. The result is a comparable heatmap.

import altair as alt

(
    long_pl.plot.rect(
        x="y:O",
        y="x:O",
        # use a Plotly-like palette; plain color="value:Q" also works
        color=alt.Color("value:Q", scale=alt.Scale(scheme="plasma")),
    )
    .properties(width=500, height=400)
)

Why this matters

Sticking with polars throughout keeps your data pipeline simple and avoids cross-library conversions. Plotly can render directly from polars data whether you choose a wide matrix with explicit y labels or tidy columns via graph_objects. This makes it easier to preserve a tidy workflow end to end, and it keeps plotting code close to the data transformation step.

Takeaways

If you already pivoted in polars, hand px.imshow the wide polars table, drop the label column from Z, and pass y explicitly. If you prefer to stay entirely in tidy form, construct go.Heatmap with x, y, and z from the corresponding polars columns and reverse the y-axis for the same orientation. Both approaches avoid converting to pandas and keep the plotting step lean.