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

what about if you change the make command to:

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

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

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

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.