Plotting matrix (heatmap/pcolor/imshow) with log scales

Hi, I’m struggling with plotting a simple matrix with log-scales for x, y and/or color-axis.
Consider e.g. a random 5*5 matrix a, with associated values for x and y axis (these values are always regularly spaced, either in linear or log space).

a = 10 .^(3*randn(5,5))
x = 10 .^(1:5)
y = 1:5

So given these values, it makes sense to use logarithmic x- and color-scales.
I tried with Plots.jl, first with GR backend:

heatmap(10 .^(1:5), 1:5, a, xscale=:log10)

which produces:
gr_xscalelog
here the xticks are perfect, but not the rendering of the matrix…
Without the xscale=:log10 option, the xticks are wrong… but the cells sizes well rendered:
gr_lincolor
Now, independently from this xticks behavior, let’s check the colors (these are probably two different problems, but I need to find a solution fixing the two together).
The zscale=:log10 seems to be ineffective, so I tried to play with the color gradient to yield a good color rendering:

heatmap(10 .^(1:5), 1:5, a, cgrad=(scale=:log10))

gr_logcolor
I don’t quite understand how the transformation is computed, but it looks worse than before, so I also tried:

heatmap(10 .^(1:5), 1:5, a, cgrad=(scale=:exp))

which is better, but still not what I expect:
gr_expcolor
This should be compared to the range of colors you get when manually computing the log:

heatmap(10 .^(1:5), 1:5, log10.(a))

which produces:
gr_manuallog
The colors are much better, but the colorbar is now useless.

Now I also tried with PyPlot backend and xscale=:log10:


The matrix display is great, but same problem with the colors.

Then I tried to use directly PyPlot, without Plots. I used the keyword argument norm=matplotlib[:colors][:LogNorm] which solves the color problem, but I can’t get the ticks to render correcly.
First with imshow, I have trouble with the xaxis, even when using xscale("log"):

PyPlot.imshow(a, cmap=:RdBu, norm=matplotlib[:colors][:LogNorm](),extent=[1, 10^5, 1, 5])


Then with pcolor, I have a bug with the first column, and also the ticks are now aligned to the begin/end of the axis, but they should be aligned with the centers of the bins. With a lin-scale I could use that and play manually with xticks, but with a log-scale it us much more annoying.

PyPlot.pcolor(10 .^(1:6),1:6, a, cmap=:RdBu, norm=matplotlib[:colors][:LogNorm]())

So none of these options works properly to me, does anyone know how to get that working?

I’m using julia 1.1.0, Plots 0.25.3, PyPlot 2.8.1.

1 Like

log scaling seems to work fine in plain GR, so it can probably easily be fixed in Plots, too. Will check this later.

using GR
heatmap(randn(5,4), xlog=true)

Thanks. With plain GR, how would you specify the values on the x axis ? because your code does indeed produces a logscale for x and does aligns the xticks properly, but on the values 1:5, and as a consequence the bins have different sizes. But the GR heatmap function does not take x and y arguments like Plots does. Also, is there a way to solve the colormap problem in plain GR? I had a quick look at the doc, but no success so far.

OK, below is the workaround I came with using PyPlot, and the output. I had trouble with my first toy example where the range for z was too high and the ticks not displaying, but it was actually unrelated to the log-scale, it works when norm=… is passed to pcolor. Regarding x- and y- ticks, I do manually compute them.

If someone knows if it is possible to achieve this behavior with Plots, I would be grateful.

Also, after thinking a bit more, the scale=:log10 option of cgrad cannot possibly solve the problem as we need to use the range of z when creating the gradient. So there should be another zscale option to use here, I probably didn’t understand correctly how to use that, or maybe it is a limitation of Plots?

a = 10 .^(1*randn(5,5));
custom_heatmap(10 .^(1:5), 1:5, a, xscale=:log10, zscale=:log10)

good

# ===== Manually extending ticks values for pcolor =====

# For n values (ticks) corresponding to the centers of n equally-spaced bins,
# returns n+1 ticks corresponding to the borders before/between/after the bins.
function extend_lin(x)
	n = length(x)
	m, M = minimum(x), maximum(x)
	halfbin = (M - m )/(2(n-1))
	return range(m - halfbin, M + halfbin, length = n+1)
end
extend_log10(x) = 10 .^(extend_lin(log10.(x)))

function extend_from_spacing(x, sp)
	if sp == :lin
		extend_lin(x)
	elseif sp == :log10
		extend_log10(x)
	else error("Unsupported spacing '$sp'.") end
end

# ===== Detecting lin- or log-spaced data =====

lindiff(x) = [x[i+1] - x[i] for i in 1:(length(x)-1)]
logdiff(x) = [x[i+1]/x[i] for i in 1:(length(x)-1)]
function findspacing(x)
	@assert length(x)>2
	lgd = unique(logdiff(x)) 	# TODO catch zeros
	if all(lgd[1] .≈ lgd)
		if lgd[1] ≈ 10
			return :log10
		elseif lgd[1] ≈ exp(1)
			return :log
		end
	end
	lid = unique(lindiff(x))
	if all(lid[1] .≈ lid)
		return :lin
	end
	@warn "Could not detect lin/log regular spacing. Using lin without any guarantees."
	:lin
end

# ===== Plotting =====

function custom_heatmap(x, y, z; 
			  clear = true, 
			  xscale = :lin, 
			  yscale = :lin, 
			  zscale = :lin, 
			  kwargs...)

	clear && clf()	# Don't know how to avoid multiple colorbars otherwise when replotting
	@assert all([s in [:lin, :log10] for s in [xscale, yscale, zscale]])
	xsp = findspacing(x)
	ysp = findspacing(y)
	nx = extend_from_spacing(x, xsp)
	ny = extend_from_spacing(y, ysp)
	xscale == :log10 && PyPlot.xscale("log")
	yscale == :log10 && PyPlot.yscale("log")
	extra_args = Dict{Symbol, Any}()
	if zscale == :log10
		extra_args[:norm] = matplotlib[:colors][:LogNorm]()
	end
	p = PyPlot.pcolor(nx, ny, z; merge(extra_args, kwargs)...)
	c = colorbar(p)
	return p
end
1 Like

In GR, the above plot can only be created using low-level functions, not with a single call. You will have to set scales, draw axes and the histogram separately - probably not what you want.

May be, Plots ha a recipe for this - I don’t know.

This might be an relatively “old” post, but I am still suffering from getting a working log color scale heatmap:

using Plots
gr()
x = LinRange(-2, 2, 40)
y = LinRange(0, pi, 20)
z = abs.(50 .* sin.(x') .+ cos.(y))
heatmap(z, cgrad=(scale=:log10))

test

Besides, I haven’t found any related documenation in either GR.jl or GRUtils.jl.

It does not seem to work in gr() but one can always heatmap log10 of the data:
heatmap(log10.(z))
log10_heatmap2

Yes, but I am wondering why the scale keyword in cgrad doesn’t do anything, even though it exists. Perhaps it would be clearer to look at the source code then, but that takes more time and effort.

Ok, I know this is not done with Plots, but if your are up to try Gnuplot.jl then the following works pretty well in every dimension, colors, x and y.

using Gnuplot
@gp exp10.(2*rand(100,100)) "w image pixels not" "set logscale cb"
@gp :- "set auto fix" "set size square" palette(:plasma)
@gp :- "set logscale x" "set logscale y" xrange = (1,100) yrange = (1,100)
save(term="pngcairo size 640,600", output="FigHeatmap_logscales.png")

4 Likes

Well… I guess this might be a bug.

Hi all,
I have to refresh this post and really need your insights as I still have a problem plotting heatmap with log colorbar even with Gnuplot.jl. I currently use the following code

@gp P_save "w image notit" "set logscale cb" 

which generates the following figure

The problem is I need to replace y axis with its real value (using another vector called Depth )rather than using index from 0 to 150. And Depth has Non-uniform grid.
Do the same for x axis too.

P_save is 150×731 Matrix{Float64}:

I am very new to both Gnuplot.jl and gnuplot.

Thank you so much!

If I understood well, with heatmap from plots.jl you can change the colums with a string array