Interactive grid plot

I want to achieve something like the following:

Basically, on clicking one of the boxes, they change the state from black and white. And there is script that changes the state of all boxes.

(link to the website: Document )

I tried using GLMakie. But I am not able to get the plot in just black and white pixels. It gets me plot in Grey scale and I am not able to update the plot as well.

(There was a post recently for updating plot using GlMakie and gave red pluses on update. But I dont know how to change the pixels individually. Link: The best approach to interact with images by clicking on them and retreiving position of the pixel I clicked on)

I thought of using Gtk’s button as black and white boxes. But again, I was not able to implement this style of boxes in glade. So I used grids and buttons. But they look ugly and its cumbersome to do in gtk anyways.

Anyone got references or tutorial or documentation for this thing?

Update: I used interpolate and GlMakie with the “on click do event” script.

using GLMakie
GLMakie.activate!(inline=false)

img = rand((0,1),(20,20))
fig, ax = image(img,interpolate = false)

on(events(fig).mousebutton, priority=0) do event
    if event.button == Mouse.left
        x, y = mouseposition(ax.scene)
        a,b = ceil(Int,x), ceil(Int,y)
        img[a,b] += 1
        img[a,b] = (img[a,b])%2
        print(a,b)
        image!(img,interpolate = false)
    end
end

Few problems with this:

  1. trunc doesnt work well, I need to implement square boxes. But I would appreciate if someone has better work around. I used ceil and it works well. Though, misbehaves if clicked outside axis.
  2. The click event doesnt work. On clicking boxes, it momentarily changes and then again goes back to original form. I tracked how it changes the value of matrix img, and it seems that somehow multi-threading is causing the problem. I would like if someone gives insight on this. how to overcome this.
1 Like

Use interpolate = false on the image plot to get boxes.

I still don’t know how to get them to update when I click a particular box

If you use an Observable for your img array and then change the values in your event handling function you can get it to change the values of the array.
This is an example where I made the relevant changes:

using GLMakie
GLMakie.activate!(inline=false)

img = Observable(rand((0,1),(20,20)))
fig, ax = image(img,interpolate = false)

on(events(fig).mousebutton, priority=0) do event
    if event.button == Mouse.left
        x, y = mouseposition(ax.scene)
        a,b = ceil(Int,x), ceil(Int,y)
        @show a, b
        img.val[a,b] += 1
        img.val[a,b] = (img.val[a,b])%2
        println("Success")
        @show a,b 
        notify(img)
    end
end

At the moment this only changes the plotting when you press ctrl while clicking on the pixel and I do not understand why this is needed. When you don’t click ctrl the value seems to be changed, but it is only flickering and does not stay on the value permanentely. The problem is that the function is called twice and I don’t understand how this can be circumvented.

2 Likes

You can also just use MakieDraw.jl PaintCanvas with a matrix of Bool. This should work out of the box.

Left and right clicks will be true/false

2 Likes

Sure. But, Idk if it’s out of updates or no-one is bothered about it, I can’t find any single documentation on that thing. I saw the video on the github repo, but I can’t find piece of code with paintcanvas.

If you know any source I missed or anything that’s helpful, please send the references.

I don’t think it was build to be tinkered like this. As mentioned in other comment by Raf, MakieDraw exist. But I can find it’s documentation.

Will try Gtk draw feature maybe.

I think this issue here is that the mouse button event gets triggered both on press and release. If you include if event.action == Mouse.press , then I believe it should only trigger once.

Here’s the modified (untested) code.

using GLMakie
GLMakie.activate!(inline=false)

img = Observable(rand((0,1),(20,20)))
fig, ax = image(img,interpolate = false)

on(events(fig).mousebutton, priority=0) do event
    if event.button == Mouse.left && event.action == Mouse.press
        x, y = mouseposition(ax.scene)
        a,b = ceil(Int,x), ceil(Int,y)
        @show a, b
        img.val[a,b] += 1
        img.val[a,b] = (img.val[a,b])%2
        println("Success")
        @show a,b 
        notify(img)
    end
end
1 Like

Youre right some how it slipped out of the readme. Its still in the tests though, doing pretty much what you want:

(FYI In Julia with a new not so well documented package you can usually find everything in the tests)

1 Like

Yes, thanks your code change works.

Makie was definitively built to be tinkered like this.
MakieDraw is doing it quite similarly just with a bit more bells and whistles and and proper checks.

1 Like

This is a version with register_interaction! which uses higher-level events. This way you can listen for a click compared to the beginning of a drag, for example:

data = Observable(rand(Bool, 30, 30))
f, ax, im = heatmap(data, colormap = [:black, :white]; axis = (; aspect = 1))

register_interaction!(ax, :toggler) do event::MouseEvent, ax
    if event.type === MouseEventTypes.leftclick
        index = round.(Int, event.data)
        if any(0 .> index .> size(data[]))
            return
        end
        data[][index...] = !data[][index...]
        notify(data)
    end
end

display(f)
1 Like

I still didnt couldnt achieve what I what I wanted to achieve. But this can be considered as solution with turning off the mouse drag event to zoom in.

GLMakie takes time to load. I still didnt try the solution by Raf. I am tired at this point for trying out all different stuff and not getting desired result. But I guess that’s beyond the scope of this post now.

For now, this does the job of changing the states of image safely. I will try MakieDraw and Gtk Canvas later. Thank you all for the help!

If my previous post doesn’t achieve what you wanted to achieve, could you clarify what’s missing?

Basically, I want to add following features

  1. Drag to fill boxes
  2. Zoom turned off (I kinda found the code for that, but its conflicting with other click events)
  3. Add grids for boxes
  4. Optional: I want to update the heatmap with some rules, so I want it to be fast and real time.

Sorry for so much inconvenience, but its my first time working with these libraries. Honestly, I realised that I can do the points 1, 2 and 3 myself if I read the documentation for Makie.

data = Observable(rand(Bool, 30, 30))
f, ax, im = heatmap(data, colormap = [:black, :white]; axis = (; aspect = 1))

hlines!(0.5:30.5, color = :gray50)
vlines!(0.5:30.5, color = :gray50)

interacted_with = Set{Point2{Int}}()

register_interaction!(ax, :toggler) do event::MouseEvent, ax
    if event.type === MouseEventTypes.leftdown
        empty!(interacted_with)
    elseif event.type in (MouseEventTypes.leftclick, MouseEventTypes.leftdrag)
        index = round.(Int, event.data)
        if any(0 .> index .> size(data[]))
            return
        end
        index in interacted_with && return
        push!(interacted_with, index)
        data[][index...] = !data[][index...]
        notify(data)
    end
end

deactivate_interaction!(ax, :rectanglezoom)

# update data programmatically
data[] .= [iseven(i) for i in 1:30, j in 1:30]
notify(data)

display(f)
1 Like

:’ ) Thank you

I tried running the code and playing around with it a bit

I had to change the two expressions in the following mod, otherwise I had an error.

hlines!(ax,0.5:30.5, color = :gray50)
vlines!(ax,0.5:30.5, color = :gray50)

If you hold down the left mouse button and start from the inside and drag the cursor “out” you get an error

julia> Error in callback:
BoundsError: attempt to access 30Ă—30 Matrix{Bool} at index   [>30 or <1]

Ah yeah I think it should be

any(1 .> index .> size(data[]))

I don’t understand the context in which this expression is inserted.
But it doesn’t sound good to me if, as I believe, size(data[ ]) >1.
It shouldn’t be index.<1 || index .>size()?

Yeah I wrote it the wrong way around, it could be yours or flipped and negated.