GLMakie: selecting points in log-log plot

Hello everyone,

I have a gui which involves a contour plot with log-log axes.
The goal is to be able to select some points from within the plot by clicking on in.

Everything works fine in the case of linear axes:

MRE for linear axes
using GLMakie

f = Figure()
ax = Axis(f[:,:],)

# make some data to plot 
x = range(0.001, 1, 100)
y = x
N = 100
σ = 15.0
μ = N / 2
z = [exp(-((x - μ)^2 + (y - μ)^2) / (2 * σ^2)) for y in 1:N, x in 1:N]

contour!(ax,x,y,z)

point = select_point(f.content[1].scene, marker=:circle)
# Update selection Observable by pushing the selected point to it
on(point) do _
    display(point)
end

but as soon as the axis get changed to log10:

MRE for log10 axes
using GLMakie

f = Figure()
ax = Axis(f[:,:], xscale = log10, yscale = log10)

# make some data to plot 
x = logrange(0.001, 1, 100)
y = x
N = 100
σ = 15.0
μ = N / 2
z = [exp(-((x - μ)^2 + (y - μ)^2) / (2 * σ^2)) for y in 1:N, x in 1:N]

contour!(ax,x,y,z)

point = select_point(f.content[1].scene, marker=:circle)
# Update selection Observable by pushing the selected point to it
on(point) do _
    display(point)
end

there are errors:

 Error in callback:
DomainError with -0.594361:
log10 was called with a negative real argument but will only return a complex result if called with a complex argument. Try log10(Complex(x)).

It seems that the select_point function returns log10.(point) rather than point, or something like that.


When I was working on this, about a year ago, I ended up using two sets of axes, one for displaying the actual log-scale values, and a linear one from 0 to 1 to selecting points from it.
This used to work, but it becomes a bit problematic now that I need to be able to zoom in and out, while keeping these axes synced.

Would there be any straightforward ways to deal with either of these issues?
That is, either select the points directly from the log-log axes without pesky errors;
or keep two axes with different values in sync.

1 Like

Sounds like a bug in Makie, should be easy enough to fix if the projection is just applied incorrectly somewhere?

Why not you supply absolute values inside log10() function?

z = abs.([exp(-((x - μ)^2 + (y - μ)^2) / (2 * σ^2)) for y in 1:N, x in 1:N])
  • Earlier i was also having same issue of evaluating log of negative values. I overcame this issue by writing safe_log() function for normalized input values.

    safe_log(x::Float64) = x > 0 ? log(x) : 1.0
    
  • Or Makie.jl should define a function that evaluates log10(-5) = -log10(5)

    safe_log(x::Float64) = x > 0 ? log(x) : x<0 ? -log(abs(x)) : NaN
    

But in this case z is already full of positive values, I don’t see how that changes things.

Thought so as well, but looking at the code for; the select_point function, it doesn’t seem obvious to me.
Perhaps it has to do with the line point = Observable([Point2f(0,0)])?

Would there be any alternatives in case that’s not straightforward to fix?

Sorry, Please find where and which variable is supplying negative value inlog10() ? Please paste full error message that you are getting. In error message we can see where log10() is getting error.

Yes, I’m trying to find that but it does not seem very obvious :slight_smile:

Here's the error
DomainError with -2.646445:
log10 was called with a negative real argument but will only return a complex result if called with a complex argument. Try log10(Complex(x)).
Stacktrace:
  [1] throw_complex_domainerror(f::Symbol, x::Float32)
    @ Base.Math ./math.jl:33
  [2] _log
    @ ./special/log.jl:330 [inlined]
  [3] log10(x::Float32)
    @ Base.Math ./special/log.jl:259
  [4] apply_transform
    @ ~/.julia/packages/Makie/ux0Te/src/layouting/transformation.jl:373 [inlined]
  [5] #995
    @ ~/.julia/packages/Makie/ux0Te/src/layouting/transformation.jl:369 [inlined]
  [6] iterate
    @ ./generator.jl:48 [inlined]
  [7] _collect(c::Vector{Point{2, Float32}}, itr::Base.Generator{Vector{Point{2, Float32}}, Makie.var"#995#996"{Tuple{typeof(log10), typeof(log10)}}}, ::Base.EltypeUnknown, isz::Base.HasShape{1})
    @ Base ./array.jl:811
  [8] collect_similar
    @ ./array.jl:720 [inlined]
  [9] map
    @ ./abstractarray.jl:3371 [inlined]
 [10] apply_transform
    @ ~/.julia/packages/Makie/ux0Te/src/layouting/transformation.jl:369 [inlined]
 [11] apply_transform
    @ ~/.julia/packages/Makie/ux0Te/src/layouting/transformation.jl:312 [inlined]
 [12] apply_transform_and_f32_conversion(float32convert::Makie.LinearScaling, transform_func::Tuple{typeof(log10), typeof(log10)}, model::StaticArraysCore.SMatrix{4, 4, Float64, 16}, data::Vector{Point{2, Float32}}, space::Symbol)
    @ Makie ~/.julia/packages/Makie/ux0Te/src/float32-scaling.jl:331
 [13] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::@Kwargs{})
    @ Base ./essentials.jl:1055
 [14] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base ./essentials.jl:1052
 [15] (::Observables.MapCallback)(value::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:436
 [16] #invokelatest#2
    @ ./essentials.jl:1055 [inlined]
 [17] invokelatest
    @ ./essentials.jl:1052 [inlined]
 [18] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [19] #70
    @ ./tuple.jl:692 [inlined]
 [20] BottomRF
    @ ./reduce.jl:86 [inlined]
 [21] afoldl
    @ ./operators.jl:553 [inlined]
 [22] _foldl_impl
    @ ./reduce.jl:68 [inlined]
 [23] foldl_impl
    @ ./reduce.jl:48 [inlined]
 [24] mapfoldl_impl
    @ ./reduce.jl:44 [inlined]
 [25] mapfoldl
    @ ./reduce.jl:175 [inlined]
 [26] foldl
    @ ./reduce.jl:198 [inlined]
 [27] foreach
    @ ./tuple.jl:692 [inlined]
 [28] (::Makie.var"#306#307"{UnionAll, Tuple{Observable{Vector{Point{2, Float32}}}}})(kw::Vector{Pair{Symbol, Any}}, args::Vector{Point{2, Float32}})
    @ Makie ~/.julia/packages/Makie/ux0Te/src/interfaces.jl:185
 [29] invokelatest(::Any, ::Any, ::Vararg{Any}; kwargs::@Kwargs{})
    @ Base ./essentials.jl:1055
 [30] invokelatest(::Any, ::Any, ::Vararg{Any})
    @ Base ./essentials.jl:1052
 [31] (::Observables.OnAny)(value::Any)
    @ Observables ~/.julia/packages/Observables/YdEbO/src/Observables.jl:420
 [32] #invokelatest#2
    @ ./essentials.jl:1055 [inlined]
 [33] invokelatest
    @ ./essentials.jl:1052 [inlined]
 [34] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [35] setindex!
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123 [inlined]
 [36] (::Makie.var"#1306#1308"{Bool, Scene, Scatter{Tuple{Vector{Point{2, Float32}}}}, Observable{Point{2, Float32}}, Observable{Vector{Point{2, Float32}}}, Observable{Bool}, Makie.Mouse.Button})(event::Makie.MouseButtonEvent)
    @ Makie ~/.julia/packages/Makie/ux0Te/src/interaction/interactive_api.jl:399
 [37] #invokelatest#2
    @ ./essentials.jl:1055 [inlined]
 [38] invokelatest
    @ ./essentials.jl:1052 [inlined]
 [39] notify
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:206 [inlined]
 [40] setindex!
    @ ~/.julia/packages/Observables/YdEbO/src/Observables.jl:123 [inlined]
 [41] (::GLMakie.var"#mousebuttons#170"{Observable{Makie.MouseButtonEvent}})(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32)
    @ GLMakie ~/.julia/packages/GLMakie/87u59/src/events.jl:104
 [42] _MouseButtonCallbackWrapper(window::GLFW.Window, button::GLFW.MouseButton, action::GLFW.Action, mods::Int32)
    @ GLFW ~/.julia/packages/GLFW/wmoTL/src/callback.jl:43
 [43] PollEvents
    @ ~/.julia/packages/GLFW/wmoTL/src/glfw3.jl:702 [inlined]
 [44] pollevents(screen::GLMakie.Screen{GLFW.Window}, frame_state::Makie.TickState)
    @ GLMakie ~/.julia/packages/GLMakie/87u59/src/screen.jl:546
 [45] on_demand_renderloop(screen::GLMakie.Screen{GLFW.Window})
    @ GLMakie ~/.julia/packages/GLMakie/87u59/src/screen.jl:1033
 [46] renderloop(screen::GLMakie.Screen{GLFW.Window})
    @ GLMakie ~/.julia/packages/GLMakie/87u59/src/screen.jl:1061
 [47] (::GLMakie.var"#79#80"{GLMakie.Screen{GLFW.Window}})()
    @ GLMakie ~/.julia/packages/GLMakie/87u59/src/screen.jl:922
Observable(Float32[-2.646445, -1.0124221])
    0 => (::var"#3#4")(::Any) @ Main REPL[18]:2

I suppose a decent temporary fix would be to plot log10.(x), log10.(y) instead of x, y and then transform the values.

The following seems to work for the tick values.
ax.xtickformat = xs -> [L"10^{%$x}" for x in xs]

Fix select_point to operate correctly in scenes with a transform_func by asinghvi17 · Pull Request #5144 · MakieOrg/Makie.jl · GitHub should fix it. Turns out we were not applying the inverse transform.

You can copy this statement and paste it into your REPL, then execute your logscale code again. Things should work after that.

Code to copy
@eval Makie begin

"""
    select_point(scene; kwargs...) -> point

Interactively select a point on a 2D `scene` by clicking the left mouse button,
dragging and then un-clicking. Return an **observable** whose value corresponds
to the selected point on the scene. In addition the function
_plots_ the point on the scene as the user clicks and moves the mouse
around. When the button is not clicked any more, the plotted point disappears.

The value of the returned point is updated **only** when the user un-clicks.

The `kwargs...` are propagated into `scatter!` which plots the selected point.
"""
function select_point(scene; blocking = false, priority=2, space = :data, kwargs...)
    key = Mouse.left
    waspressed = Observable(false)
    point = Observable([Point2f(0,0)])
    point_ret = Observable(Point2f(0,0))
    # Create an initially hidden  arrow
    plotted_point = scatter!(
        scene, point; space = :data, transformation = Makie.Transformation(scene; transform_func = identity), visible = false, marker = Circle, markersize = 20px,
        color = RGBAf(0.1, 0.1, 0.8, 0.5), kwargs...,
    )

    onany(events(scene).mousebutton, transform_func_obs(scene), priority=priority) do event, transform_func
        if event.button == key && is_mouseinside(scene)
            mp = mouseposition(scene)
            if event.action == Mouse.press
                waspressed[] = true
                plotted_point[:visible] = true  # start displaying
                point[][1] = mp
                point[] = point[]
                return Consume(blocking)
            end
          end
        if !(event.button == key && event.action == Mouse.press)
            if waspressed[] # User has selected the rectangle
                waspressed[] = false
                final_point = project(scene, :data, space, point[][1])
                if space == :data
                    final_point = apply_transform(inverse_transform(transform_func), final_point)
                end
                point_ret[] = final_point
            end
            plotted_point[:visible] = false
            return Consume(blocking)
        end
        return Consume(false)
    end
    on(events(scene).mouseposition, priority=priority) do event
        if waspressed[]
            mp = mouseposition(scene)
            point[][1] = mp
            point[] = point[] # actually update observable
            return Consume(blocking)
        end
        return Consume(false)
    end

    return point_ret
end

end
1 Like

That does it, thanks @asinghvi17!

In case I’m using this in a package, would it be wise to just copy the snippet above into it, or should I wait for the next makie release?

If you need it immediately you could just copy it - otherwise I would advise waiting for a release

1 Like