imshow and pcolormesh - showing a 2-D array, and the origin that catches everyone.
Overview
imshow
ax.imshow(z) draws a 2-D array as a grid of coloured cells.
Its defaults are chosen for images, and three of them are wrong for data.
Origin.z[0, 0] is drawn at the top-left, because that is where the first pixel of an image goes. For a matrix whose rows are a quantity — a y axis — that puts the smallest value at the top, upside down. origin="lower" fixes it.
Aspect. The default "equal" makes every cell square, so a 6×30 array is drawn five times wider than tall no matter what figure size you asked for. aspect="auto" stretches it to fill the axes.
Interpolation. The default smooths between cells. On a photograph that is desirable; on a matrix of values it draws colours that correspond to no data point, which is a small lie. interpolation="nearest" gives hard cell edges.
For data, imshow(z, origin="lower", aspect="auto", interpolation="nearest") is the honest starting point.
Worth knowing
imshow puts row 0 at the top; origin="lower" is what you want for a matrix of values.
aspect="equal" forces square cells — use aspect="auto" to fill the axes, and interpolation="nearest" so it does not invent intermediate values.
For a small matrix, write the numbers in the cells and pick the text colour by threshold so it reads on both ends of the colormap.
pcolormesh takes cell edges, so it handles uneven grids; edges outnumber cells by one per axis.
vmin/vmax force a shared colour scale — without it two heatmaps are not comparable, the same problem as unshared axes.
A diverging colormap needs symmetric limits, or its neutral midpoint lands somewhere arbitrary instead of at zero.
Images and Heatmaps
imshow and pcolormesh, and the origin that catches everyone.
imshow draws an array as a grid
Row 0 is at the TOP, because it is drawing an image.
example_01.pymatplotlib
Output
Aspect and interpolation
Two defaults that suit photographs and not data.
example_02.pymatplotlib
Output
Labelling the cells
A heatmap of a small matrix is a table, and should read like one.
example_03.pymatplotlib
Output
pcolormesh for uneven grids
When the cells are not all the same size.
example_04.pymatplotlib
Output
Setting the colour range
vmin and vmax, and why two heatmaps otherwise lie.
example_05.pymatplotlib
Output
Diverging data needs a centred scale
Or zero ends up somewhere arbitrary in the colormap.
example_06.pymatplotlib
Output
Labelling cells
A heatmap of a small matrix is a table that has been coloured. It should read like one.
Set the ticks to the row and column names, and write the values into the cells:
for i in range(rows):
for j in range(cols):
ax.text(j, i, z[i, j], ha="center", va="center")
Note the index order: ax.text(j, i, ...). The first argument is x, which is the column. Getting this backwards is the usual bug, and on a square matrix it produces a transposed result that looks plausible.
Choosing the text colour by threshold — white on dark cells, black on light — keeps every label readable. Without it, half the numbers vanish into the background at one end of the colormap.
pcolormesh
imshow assumes a regular grid and ignores any coordinates you have.
pcolormesh(x_edges, y_edges, z) takes the edges of the cells, so they can be unevenly spaced — logarithmic bins, irregular time intervals, a non-uniform mesh.
The edges outnumber the cells by one in each direction, the same convention as histogram bins. Passing centres instead of edges is the common mistake; matplotlib will accept same-sized arrays and shift everything by half a cell.
pcolormesh is slower than imshow and much more flexible. For a regular grid, imshow is the right tool.
contourf is the third option, drawing filled contour bands rather than cells — appropriate when the underlying field is genuinely continuous and misleading when it is not.
Shared colour scales
vmin and vmax set the values at the ends of the colormap.
Without them, each heatmap scales to its own data, exactly like unshared subplot axes. Two heatmaps drawn side by side then use the same colours for different values, and the comparison the layout invites is invalid.
The colorbar does say so, in numbers, and readers do not read them — they read the colours.
So: whenever two colour-mapped plots will be compared, fix vmin and vmax across both, or use one shared colorbar.
norm= gives finer control — LogNorm for data spanning orders of magnitude, BoundaryNorm for discrete bands.
Centring a diverging map
A diverging colormap has a neutral colour in the middle and two directions away from it. It only means anything if that neutral point sits at the value that matters, usually zero.
With automatic limits, the midpoint of the colormap lands at the midpoint of the data. On data ranging from −4 to +10, white sits at +3, and every cell below +3 is coloured as though it were negative.
or TwoSlopeNorm(vcenter=0) when the two sides genuinely need different ranges.
This is one of the most common quiet errors in heatmaps, because the result looks like a normal chart and asserts something false about the sign of half the data.
extent for real coordinates
imshow numbers the cells from zero by default. extent=(x0, x1, y0, y1) maps the array onto real coordinates:
The four values are the outer edges of the image, not cell centres, which is the usual off-by-half confusion. With origin="lower", y0 is the bottom.
This is what lets a heatmap be overlaid with a line or a scatter in the same units, and without it any overlay is a half-cell out.
Masked and missing values
np.nan in the array is drawn as the colormap's "bad" colour, which defaults to fully transparent — so missing cells show whatever is behind them, usually the axes background.
cmap.set_bad("0.9") makes them an explicit grey, which is better than transparent because a missing cell then reads as missing rather than as background.
A masked array behaves the same way, and is the more explicit form when the mask is computed separately from the data.
Aspect and reading order
For a correlation matrix or any square array, aspect="equal" is right and is the default.
For a wide array — time along one axis, a handful of categories along the other — aspect="auto" is necessary, or the figure is drawn at the array's proportions and ignores the size you asked for.
Row order deserves thought. An unordered heatmap makes the reader scan for structure; sorting rows by their mean, or by a clustering, often reveals the pattern the chart is meant to show. That is a data decision made in the plotting code, and it should be stated in the caption if the order is not alphabetical.
Contours
ax.contour(z) draws lines of constant value; ax.contourf(z) fills between them.
They suit genuinely continuous fields — elevation, temperature, a fitted surface — and imply smoothness, so they are wrong for a matrix of independent measurements where pcolormesh is honest.
levels= sets the number or the exact values, and ax.clabel(cs, inline=True) writes the values on the lines, which often removes the need for a colorbar.
Combining a filled contour with thin contour lines on top is a standard treatment for a field where both the bands and the exact levels matter.
Annotating a heatmap
For a small matrix, writing values into the cells makes it a table with colour as a visual aid rather than the only encoding.
The threshold trick for text colour is worth restating because it is the difference between a readable and an unreadable heatmap:
color = "white" if z[i, j] > z.max() / 2 else "black"
For a diverging map centred on zero, the threshold should be based on distance from the centre rather than from the maximum, or the labels invert on the wrong half.
Reading a heatmap
Heatmaps are read less accurately than people assume, because colour intensity is a weak encoding.
They are good at showing patterns: blocks, gradients, an outlier cell, structure along a diagonal.
They are poor at values: nobody reads 0.62 off a colour.
So the rule is that a heatmap should either be about the pattern, or it should have the numbers written in the cells — at which point it is a table with colour as a guide, which is a genuinely good display for a small matrix.
For a large matrix, the pattern is the only thing available, and ordering the rows and columns — by cluster, by total, by a meaningful sequence — is what makes a pattern visible at all.
Common heatmap mistakes
Wrong origin — row 0 at the top for data whose y axis is a quantity.
Interpolation on — smoothing between cells that are independent measurements.
Unshared scales across panels that will be compared.
An uncentred diverging map, putting the neutral colour at an arbitrary value.
No colorbar, leaving the colours meaningless.
Text the same colour everywhere, so half the labels disappear into the background.
Unordered rows, hiding whatever structure exists.
All seven are defaults or omissions rather than errors of arithmetic, which is why a heatmap can look professional and communicate nothing.
Correlation matrices
The most common heatmap in practice, and it has its own conventions.
Centre the colormap on zero with symmetric limits, because a correlation of −0.4 and +0.4 should be equally strong in opposite directions. Without it, a matrix of mostly positive correlations puts the neutral colour somewhere arbitrary and the few negatives look far more extreme than they are.
Use a diverging map for the same reason.
Set vmin=-1, vmax=1, so two matrices are comparable and so the colour scale means the same thing in every such chart.
Mask the upper triangle, since the matrix is symmetric and half of it is repetition. np.triu with np.nan and a set_bad colour does it.
Write the numbers in, if the matrix is small enough, because the exact values are usually what the reader wants.
Those five turn the default output into something readable, and the first three are the ones that affect whether it is accurate.
In summary
imshow has image defaults: row 0 at the top, square cells, and interpolation between them. For data, origin="lower", aspect="auto" and interpolation="nearest" are the honest settings.
pcolormesh takes cell edges and handles uneven grids; edges outnumber cells by one per axis.
vmin/vmax are what make two heatmaps comparable, and their absence is the same error as unshared subplot axes.
A diverging colormap needs symmetric limits or its neutral point lands at the midpoint of the data rather than at zero.
A colorbar is not optional, and a small matrix is better as a table with the numbers written in.
And row order is a choice: sorting by a meaningful quantity is frequently what makes a pattern visible at all.
Overlaying on an image
A heatmap or image often needs something drawn on top — contours, a scatter, a boundary.
The key is that imshow sets up a coordinate system, and everything overlaid must use the same one.
With default settings the cells are numbered from zero and a scatter must be in those units. With extent=, both use real coordinates and the overlay is straightforward.
Two details catch people. extent gives the outer edges, so a cell centre is half a cell in from the boundary — a scatter of cell centres plotted against edge coordinates is offset by half a cell, which looks like a small registration error.
And origin="lower" must be consistent between the image and any y coordinates computed for the overlay, or the overlay is flipped relative to the image while both look individually correct.
Drawing a few known points on top is the quickest way to confirm the coordinate system is what you think.
One more thing
ax.matshow is imshow with the defaults already set for a matrix: origin at the top with the tick labels along the top edge, which is the convention for displaying a matrix in mathematical notation.
It is convenient for a correlation matrix and confusing for anything with a quantitative y axis, where imshow(origin="lower") remains the right call.
The short version
A heatmap is read for its pattern, not its values, which is why ordering the rows and columns matters as much as the colormap.
The defaults are built for photographs, and three of them — origin, aspect and interpolation — are wrong for data. Setting all three explicitly is the honest starting point.
Reading the code back
A heatmap is one call and four corrections to its defaults: origin, aspect, interpolation and the colour limits. Add a colorbar with a label and, for a small matrix, the values in the cells. That is six lines, and it is the difference between a picture of colours and a readable display of a matrix.
Check yourself
0 of 4
Answer without scrolling back up.
Why does `imshow` put row 0 at the top?
For a matrix whose rows are a quantity, that is upside down. origin='lower' fixes it.
Why use `interpolation='nearest'` for data?
Desirable on a photograph, a small lie on a matrix of values.
What does `pcolormesh` take that `imshow` does not?
Edges outnumber cells by one per axis. Passing centres instead shifts everything by half a cell.
Data ranges from -4 to +10 with a diverging colormap and automatic limits. Where is the neutral colour?
Every cell below +3 is then coloured as though negative. Use symmetric vmin/vmax, or TwoSlopeNorm(vcenter=0).
Cheat sheet
Images and Heatmaps
Origin. z[0, 0] is drawn at the top-left, because that is where the first pixel of an image goes. For a matrix whose rows are a quantity — a y axis — that puts the smallest value at the top, upside down. origin="lower" fixes it.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.