Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Default centering of Heatmaps (and Images) on the x and y values: is this the best choice? #978

Closed
walra356 opened this issue May 24, 2021 · 10 comments

Comments

@walra356
Copy link

With regard to this question my reaction on Slack (this weekend) was "good choice" because a heatmap is based on the discrete structure of a matrix. Furthermore, many of us use heatmaps to analyze images of pixel cameras. So for those this centering seems convenient for the same reason (at least for rectangular pixel arrays).

However, at some point one usually has to relate the image to some physical scale (e.g., position on the chip); i.e., we have to move from integers (pixel indices) to reals (physical position on the chip). The latter requires a continues scale (0.0:xmax), where xmax = xdim * xscale with xscale the physical pixel repetition period of the array in the x direction. Likewise for y.

To have the best of both worlds my suggestion is to add two heatmap attributes, xscale and yscale and implement the plotrange such that:

  • for xscale = 0 (i.e., for "no scale") the plotrange defaults to the present implementation (1:xdim) - "centering on the x and y values".
  • for xscale = 1 one obtains the range (0.0:xmax) with xmax = xdim
  • for xscale = xPix one obtains the range (0.0:xdim*xPix), which corresponds to the physical size of the chip in the physical dimension chosen for xPix (or any other linear-scaling parameter of choice).
@walra356
Copy link
Author

Below I will give an example where the above is implemented in a convenience function. Is this of any use?

_irows(i::Int,ncols::Int) = (i-1)÷ncols+1 
_icols(i::Int,ncols::Int) = Base.mod1(i, ncols)
_log10_characteristic(x) = round(Int,floor(log10(x)))
_log10_mantissa(x) = log10(x)-floor(log10(x))
function _autoticks(x::Real)   
    m = _log10_mantissa(x)
    p = _log10_characteristic(x)
    v = 10^m  
    d = v > 7.9 ? 2.0 : v > 3.9 ? 1.0 : v > 1.49 ? 0.5 : 0.2
    Δx = max(1,round(Int, d *= 10^p))
    xm = (round(Int, x) ÷ Δx) * Δx
    return (-xm:Δx:xm)
end
function layout_matrices(σ, ncols=2, select=(0,); xscale=0, yscale=0, xoffset=0, yoffset=0)           
    if select != (0,) σ = [σ[i] for i ∈ select] end  
    dims = size.(σ) 
    valmin = minimum(minimum.(σ))
    valmax = maximum(maximum.(σ))
    nx = [dims[i][1] for i ∈ eachindex(σ)]
    ny = [dims[i][2] for i ∈ eachindex(σ)]  
    x0 = xscale <= 0 ? 0.5 : -xoffset
    y0 = yscale <= 0 ? 0.5 : -yoffset  
    x = xscale <= 0 ? nx : nx .* xscale
    y = yscale <= 0 ? ny : ny .* yscale 
    nrows = _irows(length(σ),ncols)
    f = Figure()
    ax = [Axis(f[_irows(i,ncols),_icols(i,ncols)]) for i ∈ eachindex(σ)]
    hm = [heatmap!(ax[i],(x0:x[i]+x0),(y0:y[i]+y0),σ[i]) for i ∈ eachindex(σ)]
    for i ∈ eachindex(σ)
        ax[i].aspect = nx[i]/ny[i]
        ax[i].xlabel = "x"
        ax[i].ylabel = "y"
        ax[i].xticks = _autoticks(x[i])
        ax[i].yticks = _autoticks(y[i])
    end
    cb = Colorbar(f[:,end+1], width = 20, height = Relative(1/2), limits = (valmin,valmax))
    return f
end

Definition of the settings:

GLMakie.activate!()
jwtheme = Theme(fontsize = 10, colormap = :gist_earth, resolution = (900,600))
set_theme!(jwtheme)
kwargs1 = (xscale = 0.0, yscale=0.0, xoffset=0, yoffset=0)
kwargs2 = (xscale = 1.0, yscale=1.0, xoffset=0, yoffset=0) 
kwargs3 = (xscale = 2.0, yscale=4.0, xoffset=2, yoffset=25)

Definitions of a matrix and a submatrix of this matrix:

n1=12; n2=9    
data = [i for i=1:n1*n2]
σ1 = reshape(data,(n1,n2))
σ2 = view(σ1,:,1:3)
σ = [σ1,σ2]
σ = rotr90.(σ)

Finally the function call:

f = plot_matrices6(σ; kwargs2...)

Enjoy the resulting plot. It raises a new question: how to set the color range. Note the color coding of the submatrix does not coincide with that of the matrix. Do we need a new attribute for this? Something like the limits attribute of the color bar?

@walra356
Copy link
Author

Sorry, the proper function call is:

f = layout_matrices(σ; kwargs2...)

@jkrumbiegel
Copy link
Member

jkrumbiegel commented May 25, 2021

I'm not sure I'm following what you're trying to do. Isn't your first point covered by this?

heatmap(0..100, 2..3, randn(20, 30))

grafik

And if you make multiple heatmaps that should have one colorbar, set colorrange = (min, max) for each heatmap.

@walra356
Copy link
Author

I first react to your second point. The colorrange specification was simply overlooked from my side. Sorry! I guess I was looking for the attribute limits (used for the colorbar). Variations in syntax (e.g., fontsize or textsize , limits or colorrange) are unfortunate but hard to avoid or eliminate...

@walra356
Copy link
Author

With regard to your first point: consider the heatmap

heatmap(1..10, 1..9, randn(10, 9))

image

Let's suppose that this heatmap represents the pixel readings of a camera - say with pixel size of 1x1 microns. First the horizontal scale. It is given in integers, nicely representing the pixel x indices 1 - 10. Likewise the y scale is centered on the index positions - be it that the numbers are not integer but unfortunately real 1.0 - 9.0.

Let's turn to the physical dimensions, obviously 10x9 microns. However, the x scale runs from 0.5 - 10.5. Likewise, 0.5 - 9.5 for the y scale. So the scale is shifted by 0.5 , which is unfortunate if you want to specify the physical position on the chip (this little shift is easily overlooked: mistakes giving rise to errors).

Such a shift is unavoidable if one moves from integers to reals. Therefore, my suggestion was to offer the option:

  • either work with integers (one-to-one mapping onto the matrix indices) 1 - 10 and 1 - 9
  • or work with reals (one-to-one mapping on the physical dimensions): 0 - 10 and 0 - 9

There are several was to implement this

  • offer an attribute integers: true/false
  • offer attributes xscale and yscale to transform from the integer scale to the real scale as I suggested above

In hindsight the true/false option is probably simplest and better.

Or did I overlook an existing attribute (e.g., what is the purpose of interpolate)?

Thanks for your attention!

@walra356
Copy link
Author

Better: discrete true/false

@jkrumbiegel
Copy link
Member

if you want to specify the bin edges instead of the bin centers, I guess it's easiest to calculate the centers from the bin edges by taking their midpoints

@walra356
Copy link
Author

I brought up the issue Is this the best choice? because (if I remember correctly) in AbstractPlotting the plotrange was defined with respect to the edges of the full colored field of the heatmap. In v0.13 the bins of the heatmap are treated in the same way as the markers of a scatter plot. I understand and respect this choice. This being said I notice that this change of convention has consequences for packages based on the old convention. For those of us working with pixel cameras it has the effect that the origin of the physical coordinates on the chip is effectively shifted by half the pixel size in x and y direction. For accurate measurements it is important to be aware of this correction.

Earlier in this discussion I was playing with the idea of offering the option of both conventions. As I understand it now the new attribute interpolate used by the DataInspecter could serve for this purpose. On the other hand as it will be more or less impossible to distill this subtlety from the documentation it may be better to close the discussion and simply start working with the convention of v.013.

Your feedback is highly appreciated!

@ffreyer
Copy link
Collaborator

ffreyer commented May 26, 2021

Maybe a bit late to mention this, but a lot of discussion on heatmaps and whatnot has happened in #748. I think the current state is kind of temporary. We added irregular heatmaps and switched to a more reasonable default with centered heatmaps, but lost some performance in the simpler cases and are not as general/tweakable as we want. I think the plan is to add an Edges at some point so that you can pass n+1 values to describe the edges of each cell. Maybe it's also usefull to have something like Left etc?

interpolate is not a new keyword btw.

@walra356
Copy link
Author

Thank you for pointing me to #748. I will keep an eye on it.

I mentioned interpolate because I found out about it in playing with the DataInspector. In view of my background I had the intuition of inspecting the surface of the chip of a pixel camera (for which centering on the edges is the preferred convention). So I was wondering whether interpolate could serve as a switch between the two centering conventions: interpolate=false <=> default convention (with only integer coordinates) and interpolate=true <=> Edges convention (and including real coordinates).

In the end most of this is a matter of taste and performance should have preference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants