biolab / orange3

🍊 :bar_chart: :bulb: Orange: Interactive data analysis
https://orangedatamining.com
Other
4.81k stars 1k forks source link

Matplotlib.pyplot issue #4572

Closed yannis72400 closed 4 years ago

yannis72400 commented 4 years ago

Describe the bug I have the following code below. It works fine when I use straight Python with made up data. When I use it in the Python widget in Orange, I cannot "unblock" the show() command, therefore I cannot export any data

Here, provide a clear and concise description of what the bug is. Although the code works in Python, it does not work in the Orange3 Python widget. I have tried to interject the a_out variable in different places but still don't see it passing through. I added global variables as you can see but still nothing. Use plt.ion()/iof() still nothing...

To Reproduce Here's the code, copy and paste to a widget and then connect any data (only one variable though. Use the select widget prior to the Python widget). You will see that it works in the first pass but then it does not work as I move the sliders right and left. Is it too complex for the widget?

from Orange.data import Domain, Table
import numpy as np
from scipy.stats.mstats import winsorize
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button

global out_data
global domain

plt.ion()
fig, ax = plt.subplots()
plt.subplots_adjust(left=0.25, bottom=0.25)

my_data=in_data.copy()
domain=in_data.domain
out_data=Table(domain,my_data)

fig.suptitle(domain)

# use asarray to convert to an array
a=np.asarray(my_data)
plt.axis([0,len(a),np.amin(a),np.amax(a)])
l1,=plt.plot(my_data,'grey')
l,=plt.plot(a,'b')

# setup the sliders
axcolor = 'lightgoldenrodyellow'
axlow = plt.axes([0.25, 0.1, 0.65, 0.03], facecolor=axcolor)
axhigh = plt.axes([0.25, 0.15, 0.65, 0.03], facecolor=axcolor)

s_low_cut = Slider(axlow, 'Low', 0.0, 1.0, valinit=.2)
s_high_cut = Slider(axhigh, 'High', 0.0, 1.0, valinit=.2)

#update when the sliders move
def update(val):
    plt.ioff()
    vlow=s_low_cut.val
    vhigh=s_high_cut.val
    a_wins=winsorize(a,limits=[vlow,vhigh], inplace=False)  
    l.set_ydata(a_wins) 
    print(vlow,vhigh)
#    l.set_ydata(a)
#    fig.canvas.draw_idle()

#print(s_low_cut.val,s_high_cut.val)
s_low_cut.on_changed(update)
s_high_cut.on_changed(update)
a_wins=winsorize(a,limits=[s_low_cut.val,s_high_cut.val], inplace=False)
out_data=Table(domain,a_wins)

#create a close button
closeax = plt.axes([0.8, 0.025, 0.1, 0.04])
button = Button(closeax, 'Close', color=axcolor, hovercolor='0.975')

def closef(event):
    global out_data
    plt.ioff()
    print(s_low_cut.val,s_high_cut.val)
    out_data=Table(domain,a_wins)
    plt.close()  
button.on_clicked(closef)

out_data=Table(domain,a_wins)

plt.show()
plt.ioff()
janezd commented 4 years ago

I fear this may be impossible. Matplotlib's documentation says

Interactive mode works with suitable backends in ipython and in the ordinary python shell, but it does not work in the IDLE IDE.

I would expect it to have similar problems in Orange, which is based on Qt. I played a bit with your script (I also added matplotlib.use('Qt5Agg'), to be sure it uses the correct back-end), but haven't succeeded. It would probably require a lot of magic to start matplotlib's event loop beside the Orange's. And it would be way way easier to simply write a proper Orange widget instead.

If you find a solution within Python Script, though, please keep us posted -- it would be interesting to know.

yannis72400 commented 4 years ago

Janez , thank you for looking into this! I will keep on working at it and see what I get.

Will keep you posted.

On Mar 27, 2020 at 05:04, <Janez Demšar (mailto:notifications@github.com)> wrote:

I fear this may be impossible. Matplotlib's documentation says

Interactive mode works with suitable backends in ipython and in the ordinary python shell, but it does not work in the IDLE IDE.

I would expect it to have similar problems in Orange, which is based on Qt. I played a bit with your script (I also added matplotlib.use('Qt5Agg'), to be sure it uses the correct back-end), but haven't succeeded. It would probably require a lot of magic to start matplotlib's event loop beside the Orange's. And it would be way way easier to simply write a proper Orange widget (https://orange-development.readthedocs.io/) instead.

If you find a solution within Python Script, though, please keep us posted -- it would be interesting to know.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub (https://github.com/biolab/orange3/issues/4572#issuecomment-604940470), or unsubscribe (https://github.com/notifications/unsubscribe-auth/AOLTFWF24RU4M64KU725UYLRJSB2BANCNFSM4LT3SORA).

ales-erjavec commented 4 years ago

matplotlib is smart enough to pick on the fact it has a running Qt event loop, so it stays in interactive mode, which means .show() will be non-blocking. I am not sure ioff/ion, have any effect here.

The Script widget however only updates outputs after the scrip finishes so only the last

out_data=Table(domain,a_wins)

before show call is ever sent.

This would work

diff --git a/Orange/widgets/data/owpythonscript.py b/Orange/widgets/data/owpythonscript.py
index 4b4b12ac7..1781fc74b 100644
--- a/Orange/widgets/data/owpythonscript.py
+++ b/Orange/widgets/data/owpythonscript.py
@@ -734,6 +734,7 @@ class OWPythonScript(OWWidget):
             one_value = all_values[0] if len(all_values) == 1 else None
             d["in_" + name + "s"] = all_values
             d["in_" + name] = one_value
+        d["send_all"] = lambda: self._send_all()
         return d

     def update_namespace(self, namespace):
@@ -753,6 +754,9 @@ class OWPythonScript(OWWidget):
         self.console.push("exec(_script)")
         self.console.new_prompt(sys.ps1)
         self.update_namespace(self.console.locals)
+        self._send_all()
+
+    def _send_all(self):
         for signal in self.signal_names:
             out_var = self.console.locals.get("out_" + signal)
             signal_type = getattr(self.Outputs, signal).type

if send_all() would then be called from the closef after setting out_data

def closef(event):
    global out_data
    plt.ioff()
    print(s_low_cut.val,s_high_cut.val)
    out_data=Table(domain,a_wins)
    send_all()
    plt.close()