heatmap with text annotations

Hi everyone!
I'm trying to plot a heatmap with text annotations in Igor Pro. Below is a minimal working example (MWE) that demonstrates what I want to achieve.

However, it seems that the 2D wave gets transposed during imaging (`AppendImage`), which makes it confusing to correctly place text annotations and set tick labels. I have to treat rows as columns (and vice versa) to compensate for this behavior, which feels counterintuitive and goes against the literal meaning of the variable names.

Is there a more elegant or idiomatic way to handle this in Igor Pro?

Here is the MWE in Igor Pro:

#pragma TextEncoding = "UTF-8"
#pragma rtGlobals=3		// Use modern global access method and strict wave access.

Function heatmap()
	// == Input Wave ==
	SetRandomSeed 3389
	make/O/N=(3,5) heatmapData = gnoise(1)
	
	// == Imaging ==
	Variable numRow = DimSize(heatmapData, 0);
	Variable numCol = DimSize(heatmapData, 1);
	Make/O/N=(numCol,numRow) heatmapDataTransposed = heatmapData[q][p]
	
	printf "(Debug) numRow: %d, numCol: %d\n", numRow, numCol
	
	Display
	AppendImage heatmapData
	ModifyImage heatmapData ctab= {*,*,Rainbow,0}
	
	make/O/N=(numCol) xTick = p
	make/O/N=(numRow) yTick = p
	
	Make/O/T/N=(numCol) xTickLabel
	Make/O/T/N=(numRow) yTickLabel
	
	Variable row, col
	String xTickStr, yTickStr
	for(col = 0; col < numCol; col++)
		sprintf xTickStr, "X%d", col
		xTickLabel[col] = xTickStr
	endfor
	
	for(row = 0; row < numRow; row++)
		sprintf yTickStr, "Y%d", row
		yTickLabel[row] = yTickStr
	endfor
	
	ModifyGraph userticks(left)={xTick,xTickLabel}
	ModifyGraph userticks(bottom)={yTick,yTickLabel}
	
	ModifyGraph width={Aspect,1}
	SetAxis/A/R left
	ColorScale/C/N=text0/F=0/Z=1/B=1/A=RC/E image=heatmapData,axisRange={-2,2}
	
	Variable xAttach
	String tagName, textLabel
	for (col=0; col < numCol; col++)
		for (row = 0; row < numRow; row++)	
			xAttach = Sub2Ind({numRow, numCol}, {row, col})
			sprintf tagName, "tag_%d_%d", row, col
			sprintf textLabel, "%0.2f", heatmapData[row][col]
			print tagName, xAttach, textLabel
			Tag/C/N=$(tagName)/F=0/B=1/Z=1/X=0.00/Y=0.00/L=0 heatmapData, xAttach,textLabel
		endfor
	endfor
End

Function Sub2Ind(dimSizes, indices)
    // dimSizes: 1D wave of sizes, e.g. {3,4,5}
    // indices:  1D wave of 0-based subscripts, e.g. {1,2,0}
    // Returns:  0-based linear index (column-major order)
    
    Wave dimSizes, indices
	Variable numDimensions = numpnts(dimSizes)
	Variable numIndices = numpnts(indices)

	if (numIndices != numDimensions)
		Abort "Number of indices must match number of dimensions"
	endif

    Variable i
    String errorMessageStr
    // Check for out-of-bounds indices
    for (i = 0; i < numDimensions; i += 1)
        if (indices[i] < 0 || indices[i] >= dimSizes[i])
	        sprintf errorMessageStr, "Index out of bounds: indices[%d] = %d, expected 0 to %d", i, indices[i], dimSizes[i] - 1
	        Abort errorMessageStr
        endif
    endfor

    Variable linearIndex = 0
    Variable stride = 1

    for (i = 0; i < numDimensions; i += 1)
        linearIndex += indices[i] * stride
		stride *= dimSizes[i]
    endfor

    return linearIndex
End

For comparison, here's a Python version using `matplotlib` that produces the expected result.  
(Note: The random values differ, possibly due to different random number generators, but that’s not a concern.)

import numpy as np
import matplotlib.pyplot as plt

def heatmap_demo():
    # Generate data
    np.random.seed(3389)
    data = np.random.normal(loc=0.0, scale=1.0, size=[3, 5])  # Shape: (rows, columns)
    print(data)

    # Define labels
    num_rows, num_cols = data.shape
    x_labels = [f"X{i + 1}" for i in range(num_cols)]
    y_labels = [f"Y{i + 1}" for i in range(num_rows)]

    # Create the heatmap
    fig, ax = plt.subplots()
    im = ax.imshow(data, cmap='rainbow')

    # Set axis ticks
    ax.set_xticks(np.arange(num_cols))
    ax.set_yticks(np.arange(num_rows))

    # Set tick labels
    ax.set_xticklabels(x_labels)
    ax.set_yticklabels(y_labels)

    # Rotate x-axis labels if needed
    plt.setp(ax.get_xticklabels(), rotation=0, ha="center", va="top")

    # Add text annotations
    for i in range(num_rows):
        for j in range(num_cols):
            text = f"{data[i, j]:.2f}"
            ax.text(j, i, text, ha="center", va="center", color="black")

    # Make layout tight and show
    plt.title("Heatmap with Custom Tick Labels")
    plt.colorbar(im)
    plt.tight_layout()
    plt.show()

def main():
    heatmap_demo()

if __name__ == "__main__":
    main()

 

Heatmap with Custom Tick Labels Generated Using Python Heatmap with Custom Tick Labels Generated Using Igor Pro
chozo

You may be confused by the table and the image representations of an array within Igor. Note that the 'rows' are always displayed on the horizontal / x axis both in image and line plots while columns run along the vertical / y axis (unless you deliberately swap axes). In a table, however, the representation is flipped, i.e., rows always run vertical and columns horizontal in a table. This has apparently historic reasons, about which you can read more here:

https://www.wavemetrics.com/forum/general/translating-matlab-matrix-mul…

In reply to by ChrLie

guoqilin

ChrLie wrote:

what about if you change the make command to:

make/O/N=(5,3) heatmapData = gnoise(1)

Thank you for your reply! I worked around the issue in a similar way by using heatmapDataTransposed instead of heatmapData, which makes me feel better.

Function heatmap_2()
	// Wave: column-major storage; Image: bottom-left origin
	// == Input Wave ==
	SetRandomSeed 3389
	make/O/N=(3,5) heatmapData = gnoise(1)
	
	// == Imaging ==
	Variable numRow = DimSize(heatmapData, 0);
	Variable numCol = DimSize(heatmapData, 1);
	Make/O/N=(numCol,numRow) heatmapDataTransposed = heatmapData[q][p]
	
	printf "(Debug) numRow: %d, numCol: %d\n", numRow, numCol
	
	Display
	AppendImage heatmapDataTransposed
	ModifyImage heatmapDataTransposed ctab= {*,*,Rainbow,1}
	
	make/O/N=(numCol) xTick = p
	make/O/N=(numRow) yTick = p
	
	Make/O/T/N=(numCol) xTickLabel
	Make/O/T/N=(numRow) yTickLabel
	
	Variable row, col
	String xTickStr, yTickStr
	for(col = 0; col < numCol; col++)
		sprintf xTickStr, "X%d", col
		xTickLabel[col] = xTickStr
	endfor
	
	for(row = 0; row < numRow; row++)
		sprintf yTickStr, "Y%d", row
		yTickLabel[row] = yTickStr
	endfor
	
	ModifyGraph userticks(bottom)={xTick,xTickLabel}
	ModifyGraph userticks(left)={yTick,yTickLabel}
	
	ModifyGraph width={Aspect,1},mirror=2,axisOnTop=1,standoff=0
	SetAxis/A/R left
	ColorScale/C/N=text0/F=0/Z=1/B=1/A=RC/E image=heatmapDataTransposed //,axisRange={-2,2}
	
	Variable xAttach
	String tagName, textLabel
	for (col=0; col < numCol; col++)
		for (row = 0; row < numRow; row++)	
			xAttach = Sub2Ind({numCol, numRow}, {col, row})
			sprintf tagName, "tag_%d_%d", row, col
			sprintf textLabel, "%0.2f", heatmapDataTransposed[col][row]
			print tagName, xAttach, textLabel
			Tag/C/N=$(tagName)/F=0/B=1/Z=1/X=0.00/Y=0.00/L=0 heatmapDataTransposed, xAttach,textLabel
		endfor
	endfor
End

 

 

 

Heatmap Using Igor Pro (version 2) (37.59 KB)

In reply to by chozo

guoqilin

chozo wrote:

You may be confused by the table and the image representations of an array within Igor. Note that the 'rows' are always displayed on the horizontal / x axis both in image and line plots while columns run along the vertical / y axis (unless you deliberately swap axes). In a table, however, the representation is flipped, i.e., rows always run vertical and columns horizontal in a table. This has apparently historic reasons, about which you can read more here:

https://www.wavemetrics.com/forum/general/translating-matlab-matrix-mul…

Thank you for clearing that up.