Makie: nonlinear color levels in Colorbar

In a contourf map, I sometimes need to show the structure of the field at small absolute values as well as showing large absolute values. At the same time, I’d like to enable the reader to “read” approximate values off the map. For example, the first map near the bottom of this message is the sea-surface temperature anomaly of January from the annual average. The contour levels are -2, -1, -0.5, -0.2, 0.2, 0.5, 1, 2 as the color bar shows. You can easily sea which region has which temperature value range.

On the other hand, the following sample program generates the second attached plot. The contour intervals are exactly as intended, but the colorbar remains “linear” unlike in the first plot.

using CairoMakie

xs = 0.0:1.0:10.0
ys = 0.0:1.0:7.0

func(x,y) = exp((x^2 + y^2)/20)
vals = func.(xs, ys')
levels = [2, 5, 10, 20, 50, 100, 200]

fig = Figure()
ax = Axis(fig[1,1])
c = contourf!(ax, xs, ys, vals,
              levels=levels, extendlow=:auto, extendhigh=:auto)
Colorbar(fig[1,2],c)
save("tmp2.png", fig)

It’s not easy to see which value range each color represents.

Is there an option to Makie to achieve a nonlinear colorbar easily?

The method I’ve thought of so far is very tedious:

# Take log of the field for negative values and positive values
logtemp = (x -> (x > eps) ? log(x) : (x < -eps) ? log(-x) : 0.0).(temp)
# Plot log(temp)
c = contourf!( . . . , logtemp, levels = . . . )
# Manually put labels like "-0.2" on the colorbar
Colormap(  . . . )

The log part would be easy, but calculating the labels on the colorbar would be tedious.

The replies I’ve gotten at this post will help you make a nonlinear / nonuniform color scale I believe :slight_smile:

1 Like

Thanks! this answer of @jules’s is closest to what I want to achieve: You scale the color scheme using the parameter

colorscale=ReversibleScale(conversion_func, inverse_conversion_func)

to contourf. I paste a my sample code after incorporating the idea.

The only remaining issue is the labeling of the colorbar. The labels are still placed at a linear interval (it’s 50 in the following example), missing finer contour levels.

It would be nice if the tickmarks of Colorbar tried to follow the levels specified as levels=[ . . .].

I’ll investigate.

using CairoMakie

xs = 0.0:1.0:10.0
ys = 0.0:1.0:7.0

func(x,y) = exp((x^2 + y^2)/20)
vals = func.(xs, ys')
levels = [2, 5, 10, 20, 50, 100, 200]

scaling = let thr = 2.0, lthr = log(thr)
  function scale(x)
    (x > thr) ? log(x) : ((x < -thr) ? -log(-x) : (x/thr)*lthr)
  end
  function invscale(x)
    (x > lthr) ? exp(x) : ((x < -lthr) ? -exp(-x) : (x/lthr)*thr)
  end
  CairoMakie.ReversibleScale(scale, invscale)
end

fig = Figure()
ax = Axis(fig[1,1])
c = contourf!(ax, xs, ys, vals; colorscale=scaling
              ,levels = levels
              ,extendlow=:auto, extendhigh=:auto)
Colorbar(fig[1,2],c)
save("tmp2.png", fig)


j

I’ve found a solution!

Colorbar(fig[1,2],c; ticks=levels)

My last solution was fine, but the intervals in the colorbar was uneven.

In this method, you map your original data “v” using a monotonic function “u = f(v)” only for shading purposes: colorscale = . The contour interval is linear in the “u” space. To get a uniform interval for levels = [-5, -2, -1, -0.5, . . . ], therefore, you have to invent a map such that f.([-5,-2,-1, . . .]) is linear. In my last attempt, this property didn’t hold, which was the reason for the uneven intervals on the colorbar.

So, I’ve written a function to create such a map and its inverse:

"""
 To be used as `colorscale` of Makie's `contourf` and `heatmap`.
 Picewise linear mapping such that
   func.([v1,v2, . . . , vn]) == [0, 1, . . . , n-1].
 Outside the range [v1, vn], it's the simple lienar extrapolation.
 vs = [v1,v2, . . . , vn] must be strictly increasing.
"""
function mk_piecewise_linear(vs)
  @assert length(vs) > 1
  function is_increasing(ss)
    prev = ss[1]
    for s in ss[2:end]
      (s <= prev) && return false
      prev = s
    end
    return true
  end
  @assert is_increasing(vs)
  d1 = vs[2] - vs[1]
  d2 = vs[end] - vs[end-1]
  un = size(vs,1) - 1
  function piecewise_linear(v)
    if v <= vs[1]
      (v - vs[1])/d1
    elseif v >= vs[end]
      (v - vs[end])/d2 + un
    else
      i = findfirst(q -> v < q, vs) - 1
      d = vs[i+1] - vs[i]
      (v - vs[i])/d + i-1
    end
  end
  function its_inverse(u)
    if u <= 0.0
      u*d1 + vs[1]
    elseif u >= un
      (u - un)*d2 + vs[end]
    else
      iu = floor(Int,u)
      i = iu + 1
      d = vs[i+1] - vs[i]
      (u - iu)*d + vs[i]
    end
  end
  return CairoMakie.ReversibleScale(piecewise_linear, its_inverse)
end

Using this function, you can finally use arbitrary contour levels like

using CairoMakie

func(x,y) = x

levels = [-5, -2, -1, -0.5, 0.5, 1, 2, 5]

include("mk_piecewise_linear.jl")
scaling = mk_piecewise_linear(levels)

xs = -7.0:1.0:7.0
ys = 0.0:1.0:7.0
vals = func.(xs, ys')
fig = Figure()
ax = Axis(fig[1,1])
c = contourf!(ax, xs, ys, vals
              ,colorscale=scaling
              ,levels = levels
              ,extendlow=:auto, extendhigh=:auto)
Colorbar(fig[1,2],c; ticks=levels)
save("tmp.png", fig)

This is the result I wanted, except the levels -2.0 and 2.0 are missing from the colorbar, which I think is an unrelated bug in Makie, which I’ve already submitted a report for.