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.