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.

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

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.