MurrellGroup / ProtPlot.jl

Protein ribbon plots implemented in Julia using Makie
MIT License
14 stars 1 forks source link

Ribbon Observables #27

Open anton083 opened 1 month ago

anton083 commented 1 month ago

It would be nice if the ribbon would re-render every time the backbone coordinates of the chains changed. Oxygen atoms and secondary structure would have to be re-assigned.

Here's my attempt at implementing it:

function Makie.plot!(ribbon::Ribbon{Tuple{Vector{Protein.Chain}}})
    coords_observables = [Observable(chain.backbone.coords) for chain in ribbon[1][]]

    function update_plot(coords_observables...)
        chains = deepcopy(ribbon[1][])
        Protein.assign_oxygens!.(chains)
        _assign_secondary_structure!(chains)
        isnothing(ribbon.colors[]) && (ribbon.colors = [LinRange(0, 1, length(chain)) for chain in chains])
        empty!(ribbon.plots)
        render!(ribbon, chains)
    end

    Makie.Observables.onany(update_plot, coords_observables...)

    update_plot(coords_observables...)

    return ribbon
end

This doesn't work because Observables doesn't detect mutation of coords, and the user can also not reach inside to update coords_observables and trigger update_plot. We can wrap coords in whatever, but the user will never have access to that thing, and Observables doesn't listen to mutations, I think -- only to e.g. obs = Observable(0); obs[] = 1.

Alternatively we could instead define Makie.plot!(ribbon::Ribbon{Tuple{Observable{Vector{Protein.Chain}}}}) such that the user could update a passed observable. The user wouldn't necessarily need to pass an observable if we define a convert_arguments for this.

timholy commented 1 month ago

Is this possibly related to animated visualizations? I'm working on something similar from a different angle, see https://github.com/HolyLab/FlyThroughPaths.jl (just created yesterday). There will be more modes added, and thankfully one of the Makie developers has gotten involved (see https://github.com/HolyLab/FlyThroughPaths.jl/pull/6).

If you look at the implementation, you'll see there's even an action field which can be a callback (not yet documented). I'm planning on using this to adjust things like chain and residue transparency, so that as one flies around the protein one can gradually emphasize structural points that you want to highlight.

asinghvi17 commented 1 month ago

In this case I don't think you're using the Observables right. ribbon[1] is an Observable, so you want to create either one observable (@lift([chains.backbone.coords for chain in $(ribbon[1])])) or lift on ribbon[1] and update multiple pre-created observables. With the first approach, you don't need to splat which is nice.