2025, Sep 28 03:00
Altair heatmap borders disappear after save(): why stroke='w' breaks PNG/SVG/PDF exports and how to fix it
Altair heatmap borders vanish in exports (PNG/SVG/PDF) when using stroke='w'. Fix by switching to valid colors like 'white' or '#eeeeee' for consistent results.
Altair heatmaps can look perfect in a Jupyter notebook and still lose their cell borders when exported to PNG, SVG, or PDF via save(). If your notebook preview shows thick grid lines but the saved images show razor-thin lines or no borders at all, the culprit may be a seemingly harmless color shorthand.
Reproducing the issue
The following example renders a heatmap with wide borders in the notebook, yet the exported images end up with missing or ultra-thin strokes.
import altair as alt
import pandas as pd
# sample dataset
frame_src = pd.DataFrame({
    'X': [1,2,3,4,5,6,7,8,9,10],
    'Y': [1,2,3,4,5,6,7,8,9,10],
    'Intensity': [0,0,0,0,0,1,1,1,1,1]
})
levels = [0, 1]
palette = ['#208943', '#c0e6ba']
# heatmap with thick stroke, looks fine in notebook
map_fig = alt.Chart(frame_src).mark_rect(stroke='w', strokeWidth=5).encode(
    x=alt.X('X:N'),
    y=alt.Y('Y:N'),
    color=alt.Color('Intensity:N', scale=alt.Scale(domain=levels, range=palette))
)
# exports that may drop or thin the borders
map_fig.save('test.png')
map_fig.save('test.svg')
map_fig.save('test.pdf')
map_fig
What’s going on
This behavior aligns with a known Altair/Vega issue triggered by using stroke='w'. The value w is not a valid HTML color string. While the notebook renderer shows the borders as expected, the export pipeline does not interpret w as a valid color and the stroke ends up missing or rendered inconsistently. That explains why PNG/SVG might show extremely thin lines in some cases, and PDF may drop the borders entirely.
The fix
Use a valid color value for the stroke. Switching to a proper color name such as 'white' restores consistent borders across notebook preview and exported files. If pure white blends too much into the background on your setup, a light gray like '#eeeeee' can improve visibility while maintaining the same layout.
import altair as alt
import pandas as pd
# sample dataset
frame_src = pd.DataFrame({
    'X': [1,2,3,4,5,6,7,8,9,10],
    'Y': [1,2,3,4,5,6,7,8,9,10],
    'Intensity': [0,0,0,0,0,1,1,1,1,1]
})
levels = [0, 1]
palette = ['#208943', '#c0e6ba']
# corrected: use a valid color string for the stroke
map_fig_fixed = alt.Chart(frame_src).mark_rect(stroke='white', strokeWidth=5).encode(
    x=alt.X('X:N'),
    y=alt.Y('Y:N'),
    color=alt.Color('Intensity:N', scale=alt.Scale(domain=levels, range=palette))
)
map_fig_fixed.save('test.png')
map_fig_fixed.save('test.svg')
map_fig_fixed.save('test.pdf')
map_fig_fixed
Alternative for better visibility:
map_fig_gray = alt.Chart(frame_src).mark_rect(stroke='#eeeeee', strokeWidth=5).encode(
    x=alt.X('X:N'),
    y=alt.Y('Y:N'),
    color=alt.Color('Intensity:N', scale=alt.Scale(domain=levels, range=palette))
)
map_fig_gray.save('test.png')
Why this matters
Charts are often embedded into reports, PDFs, and dashboards. A styling tweak that appears fine in the notebook but vanishes in exported files can undermine readability and consistency. Using valid color specifications prevents that mismatch and keeps your visualization faithful across environments and formats.
Takeaways
If borders disappear or look inconsistent in Altair exports, check your color values first. Avoid shorthand like w and stick to valid HTML color names or hex codes. A simple change like stroke='white' or a subtle stroke='#eeeeee' is enough to make the saved images match what you see in Jupyter.