Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

using magicclass and popup boxes #2

Closed
pr4deepr opened this issue Nov 3, 2021 · 13 comments
Closed

using magicclass and popup boxes #2

pr4deepr opened this issue Nov 3, 2021 · 13 comments

Comments

@pr4deepr
Copy link

pr4deepr commented Nov 3, 2021

Hi @hanjinliu
Thanks for this amazing tool. I've been enjoying playing around with it.

I was looking at this example using pandas and seaborn
When clicking each button, a popup window appears.
image

Instead of a popup window, how can I have the fields appear in the main window (in this case, above the Swarm Plot button)? Also, my aim is that once the user enters the values within the main window and clicks the Swarm Plot button it just plots it using the values without an additional box?

I can see you've used field or magicgui before, but where do I add that if I was following a similar workflow as your seaborn example?

Cheers
Pradeep

@pr4deepr pr4deepr changed the title using magicclass in napari using magicclass and popup boxes Nov 3, 2021
@hanjinliu
Copy link
Owner

Hi @pr4deepr,

I'm happy to hear you enjoy coding with magic-class!

  1. About popups.

I'm sorry, I need more time to make it work because there would be a lot of options to open the magicgui widget (such as in a napari-like way, in the last position or right below the button as you mentioned). You can suppress popup with option @magicclass(popup=False) for now, but this option still has some bugs (some of them are fixed in the latest 0.5.4dev0).

  1. How to use field in this example.

First, since you want to let fields like x and y appear in the Plot_Menu widget, these fields should be defined inside the Plot_Menu class.

class Plot_Menu:
    x = field(str)
    y = field(str)
    hue = field(str)
    dodge = field(bool)
    def Plot(self): ...
    def Histogram(self): ...

Every time seaborn's plot function is called, you have to refer to those fields. Since _seaborn_plot function is a member of the class Analyzer, those fields should be accessed via self.Tools.Plot_Menu. Modified functions would look like:

class Analyzer:

    ...

    def _seaborn_plot(self, plot_function):
        x = self.Tools.Plot_Menu.x.value or None
        y = self.Tools.Plot_Menu.y.value or None
        hue = self.Tools.Plot_Menu.hue.value or None
        dodge = self.Tools.Plot_Menu.dodge.value
        self.canvas.figure.clf()
        df = self._current_df()
        plot_function(data=df, ax=self.canvas.ax, x=x, y=y, hue=hue, dodge=dodge)
        self.canvas.draw()

    @Tools.Plot_Menu.wraps
    def Box_Plot(self):
        """Show box plot"""        
        self._seaborn_plot(sns.boxplot)

and the main window will look like this

image

@pr4deepr
Copy link
Author

pr4deepr commented Nov 4, 2021

Hi @hanjinliu
This is great. Thanks for the quick reply.
In regard to location of the popup, I can see why it gets complex!! Can it be done so that it appears above or below the button as an initial implementation?

I am mainly using this in napari, but I thought I'd try your examples first to get an understanding.
When using @magicclass(popup=False) in napari, the popup box doesnt appear, but I have to click it twice so that it reappears.

Also, another thing I noticed is that the pop up boxes don't dock onto napari. Why is that?

Cheers
Pradeep

@hanjinliu
Copy link
Owner

Hi @pr4deepr,

Yes, I was aware of the bug and it's fixed now.

I've also added some popup options in the main branch. Can you clone it and give it a try? Instead of @magicclass(popup=False), you can use @magicclass(popup_mode="dock") to dock popup widgets in napari (although only docking in the right area is supported for now). It's compatible with close_on_run option, so you can use @magicclass(popup_mode="dock", close_on_run=False) to prevent them from being closed every time.

Currently implemented popup modes are: 'first' (top of the widget), 'last' (bottom of the widget), 'dock', 'above' (above the button), 'below' (below the button) and 'parentlast' (bottom of the most parent widget). I hope they'll be helpful.

@pr4deepr
Copy link
Author

pr4deepr commented Nov 4, 2021

Thank you so much.

So, when I run the code like this in napari:

from magicclass  import magicclass
from pathlib import Path
import napari
from skimage import io

@magicclass
class test_widget:
    @magicclass(widget_type="toolbox", name = "Analysis")
    class Tools:
        @magicclass(popup_mode="dock")
        class File_Menu:
            def Open_File(self,path:Path):
                print("Opening", path)
                img=io.imread(path)
                viewer.add_image(img)

widget = test_widget()

viewer=napari.Viewer()
viewer.window.add_dock_widget(widget)
napari.run()

I can click Open file from the napari window and it works fine.

However, when using the napari plugin system generated by napari cookiecutter, followed by calling this from the napari plugin menu, I get an error:
NoneType has no attribute: add_image.

I'm assuming its with how I initialise or define the napari viewer, but I can't figure out where to define this appopriatelY?


@magicclass
class test_widget:
    @magicclass(widget_type="toolbox", name = "Analysis")
    class Tools:
        @magicclass(popup_mode="dock")
        class File_Menu:
            def Open_File(self,path:Path, viewer:Viewer):
                print("Opening", path)
                img=io.imread(path)
                viewer.add_image(img)
              
@napari_hook_implementation
def napari_experimental_provide_dock_widget():
    return [(test_widget, {"name" : "Analysis widget"} )]

Apologies if the questions are too basic.

@hanjinliu
Copy link
Owner

I'm not sure what's going on when napari registers its Viewer object to magicgui, so I don't have a correct answer to your question...
But magicclass objects have a property parent_viewer, which returns a pointer to its parent napari.Viewer object. Can you try this code?

def Open_File(self, path: Path):
    print("Opening", path)
    img = io.imread(path)
    self.parent_viewer.add_image(img)

@pr4deepr
Copy link
Author

pr4deepr commented Nov 4, 2021

Ok, that works really well.
Thanks a lot @hanjinliu

BTW, is there any link to documentation for magic-class? There is so much good stuff in here!!
Happy to help in anyway..

@pr4deepr
Copy link
Author

pr4deepr commented Nov 4, 2021

I've posted this on imagesc forum to get input from napari developers about accessing the viewer.
https://forum.image.sc/t/problem-accessing-napari-viewer-with-magic-class/59680/2
I don't know if you're on imagesc..

@hanjinliu
Copy link
Owner

is there any link to documentation for magic-class?

I admit that's a high-priority task, so please wait for a while... It would be great if you'll visit the documentation and send some feedbacks to me.

I've posted this on imagesc forum to get input from napari developers about accessing the viewer.

Thank you very much! The discussion there are also helpful for me. I have not joined imagesc yet, but it seems that I should also sign up!

@pr4deepr
Copy link
Author

pr4deepr commented Nov 5, 2021

Yea, I'd recommend you join imagesc. I think there was a question for you by Talley on the forum:
https://forum.image.sc/t/problem-accessing-napari-viewer-with-magic-class/59680/3?u=pr4deepr

BTW, I am having some more trouble now.
If I run this piece of code using napari plugins to blur an image for example:

from napari.types import ImageData
from magicgui import magicgui
from napari import Viewer
from napari_plugin_engine import napari_hook_implementation

from magicclass import magicclass
from pathlib import Path
from skimage import io
from skimage.filters import gaussian

@magicclass(widget_type="scrollable")
class test_widget:
        @magicclass(widget_type="list", popup_mode="last")
        class File_Menu:
            def Open_File(self,path:Path, viewer:Viewer): ...

            @magicgui
            def blur(self,sigma:int): ...          
        
        @File_Menu.wraps
        def Open_File(self,path:Path):
            print("Opening", path)
            img=io.imread(path)
            self.parent_viewer.add_image(img)  


        @File_Menu.wraps
        
        def blur(self,img_data:ImageData,sigma:int):  
            print("Blurring")
            blurred_img=gaussian(img_data,sigma)
            self.parent_viewer.add_image(blurred_img)

@napari_hook_implementation
def napari_experimental_provide_dock_widget():
    return [(test_widget, {"name" : "Analysis widget"} )]

I get this in napari:
image
But, when entering a sigma value and clicking Run, it doesn't work or execute the "blurring" function.

However, when I move the blur function to be under the File_Menu class, it works now:

from napari.types import ImageData
from magicgui import magicgui
from napari import Viewer
from napari_plugin_engine import napari_hook_implementation

from magicclass import magicclass
from pathlib import Path
from skimage import io
from skimage.filters import gaussian

@magicclass(widget_type="scrollable")
class test_widget:
        @magicclass(widget_type="list", popup_mode="last")
        class File_Menu:
            def Open_File(self,path:Path, viewer:Viewer): ...

            @magicgui
            def blur(self,img_data:ImageData, sigma:int):           
                print("Blurring")
                blurred_img=gaussian(img_data,sigma)
                self.parent_viewer.add_image(blurred_img)
                
        @File_Menu.wraps
        def Open_File(self,path:Path):
            print("Opening", path)
            img=io.imread(path)
            self.parent_viewer.add_image(img)  

        #@File_Menu.wraps
        #def blur(self,img_data:ImageData,sigma:int):  
            #print("Blurring")
            #blurred_img=gaussian(img_data,sigma)
            #self.parent_viewer.add_image(blurred_img)

@napari_hook_implementation
def napari_experimental_provide_dock_widget():
    return [(test_widget, {"name" : "Analysis widget"} )]

@pr4deepr
Copy link
Author

pr4deepr commented Nov 5, 2021

Happy to provide feedback on documentation. Let me know if/when you have a link

@hanjinliu
Copy link
Owner

The compatibility of wraps decorator and magicgui decorator is a little bit tricky...
The following code should work. But sorry again, I found a bug while debugging your code, so could you clone the latest code again?

@magicclass(widget_type="scrollable")
class test_widget:
    @magicclass(widget_type="list", popup_mode="last")
    class File_Menu:
        def Open_File(self, path:Path): ...
        def blur(self,sigma:int): ...          

    @File_Menu.wraps
    def Open_File(self,path:Path):
        print("Opening", path)
        img=io.imread(path)
        self.parent_viewer.add_image(img)  

    @File_Menu.wraps
    @magicgui
    def blur(self,img_data:ImageData,sigma:int):  
        print("Blurring")
        blurred_img=gaussian(img_data,sigma)
        self.parent_viewer.add_image(blurred_img)

If you predefined blur function with @magicgui, then @magicclass convert it into a FunctionGui with an empty function. @magicclass searches for wrapped function in the parent class (like the function below @File_Menu.wraps) only when it encounters a normal method in the child class. That's why you found a FunctionGui in the widget but no response occurred when you click it.

The predefinition/wraps strategy is needed only when you want to call parent method from its child widget. In your case, you don't have to use this strategy because the blur function don't have to know its parent state. You can just keep using the second code you showed.

Happy to provide feedback on documentation

That's great! Thanks for your help and interest in magic-class!

@pr4deepr
Copy link
Author

pr4deepr commented Nov 5, 2021

Thanks. Eventually, I would like to design a sequence of widgets, where:

  • open a file and read metadata within the Open_file widget, and assign these attributes to parent class test_widget.
  • Access the new attributes created across other widgets under test_widget class.

Is this the right approach?

@hanjinliu
Copy link
Owner

Ah yes, in that case you'll have to use wraps decorator.

@pr4deepr pr4deepr closed this as completed Nov 6, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants